<!--
  /admin/profile — every authenticated principal lands here for their
  own identity + API-key management. No role gating: a member can mint
  keys for the apps they belong to just like an admin can. Users-admin
  actions live under /admin/users.
-->
<script lang="ts">
	import { onMount } from 'svelte';
	import { page } from '$app/state';
	import {
		api,
		ApiError,
		ALL_SCOPES,
		isInstanceScope,
		type ApiKeyDto,
		type App,
		type MintApiKeyResponse,
		type Scope
	} from '$lib/api';
	import { currentUser } from '$lib/auth';
	import RoleChip from '$lib/RoleChip.svelte';
	import ConfirmModal from '$lib/ConfirmModal.svelte';

	const me = $derived($currentUser);

	let keys = $state<ApiKeyDto[]>([]);
	let apps = $state<App[]>([]);
	let appBySlug = $derived(new Map(apps.map((a) => [a.id, a])));
	let loadError = $state<string | null>(null);
	let banner = $state<{ kind: 'error' | 'info'; message: string } | null>(null);

	// Surface the cross-page "access denied" notice when /users bounces
	// a member back here. One-shot — clears as soon as the user
	// navigates away or dismisses.
	const deniedFromUsers = $derived(page.url.searchParams.get('denied') === 'users');

	let mintOpen = $state(false);
	let mintForm = $state<{
		name: string;
		scopes: Set<Scope>;
		app_id: string | '';
		expires_at: string;
	}>({ name: '', scopes: new Set(), app_id: '', expires_at: '' });
	let mintPending = $state(false);
	let mintError = $state<string | null>(null);

	let reveal = $state<MintApiKeyResponse | null>(null);
	let revealAck = $state(false);
	let copyState = $state<'idle' | 'copied'>('idle');

	let revokeTarget = $state<ApiKeyDto | null>(null);
	let revokePending = $state(false);

	const NAME_MAX = 64;
	const scopeIsInstance = (s: Scope) => isInstanceScope(s);
	const boundToApp = $derived(mintForm.app_id !== '');

	const canSubmit = $derived(
		mintForm.name.trim().length > 0 &&
			mintForm.name.trim().length <= NAME_MAX &&
			mintForm.scopes.size > 0 &&
			!mintPending
	);

	onMount(async () => {
		await Promise.all([refreshKeys(), loadApps()]);
	});

	async function refreshKeys() {
		try {
			keys = await api.apiKeys.list();
			loadError = null;
		} catch (e) {
			loadError = e instanceof ApiError ? e.message : 'failed to load API keys';
		}
	}

	async function loadApps() {
		try {
			apps = await api.apps.list();
		} catch {
			// Non-fatal: the form falls back to "no app options" and the
			// list shows the bare UUID in the binding column.
			apps = [];
		}
	}

	function flash(kind: 'error' | 'info', message: string) {
		banner = { kind, message };
		setTimeout(() => {
			if (banner?.message === message) banner = null;
		}, 6000);
	}

	function openMint() {
		mintForm = { name: '', scopes: new Set(), app_id: '', expires_at: '' };
		mintError = null;
		mintOpen = true;
	}

	function cancelMint() {
		mintOpen = false;
		mintError = null;
	}

	function toggleScope(s: Scope) {
		const next = new Set(mintForm.scopes);
		if (next.has(s)) next.delete(s);
		else next.add(s);
		mintForm = { ...mintForm, scopes: next };
	}

	// When the user binds the key to an app, instance:* scopes are
	// mutually exclusive — drop them from the selection so submit
	// doesn't 422.
	$effect(() => {
		if (!boundToApp) return;
		const filtered = new Set<Scope>();
		let dropped = false;
		for (const s of mintForm.scopes) {
			if (scopeIsInstance(s)) dropped = true;
			else filtered.add(s);
		}
		if (dropped) {
			mintForm = { ...mintForm, scopes: filtered };
		}
	});

	async function submitMint(event: SubmitEvent) {
		event.preventDefault();
		if (!canSubmit) return;
		mintPending = true;
		mintError = null;
		try {
			const r = await api.apiKeys.mint({
				name: mintForm.name.trim(),
				scopes: Array.from(mintForm.scopes),
				app_id: mintForm.app_id === '' ? null : mintForm.app_id,
				expires_at: mintForm.expires_at === ''
					? null
					: new Date(mintForm.expires_at + 'T23:59:59Z').toISOString()
			});
			reveal = r;
			revealAck = false;
			copyState = 'idle';
			mintOpen = false;
			await refreshKeys();
		} catch (e) {
			mintError = e instanceof ApiError ? e.message : 'failed to mint API key';
		} finally {
			mintPending = false;
		}
	}

	async function copyToken() {
		if (!reveal) return;
		try {
			await navigator.clipboard.writeText(reveal.raw_token);
			copyState = 'copied';
			setTimeout(() => (copyState = 'idle'), 2000);
		} catch {
			flash('error', 'Clipboard write failed — select and copy manually.');
		}
	}

	function dismissReveal() {
		reveal = null;
		revealAck = false;
	}

	function openRevoke(key: ApiKeyDto) {
		revokeTarget = key;
	}

	async function confirmRevoke() {
		if (!revokeTarget) return;
		revokePending = true;
		const target = revokeTarget;
		try {
			await api.apiKeys.revoke(target.id);
			revokeTarget = null;
			keys = keys.filter((k) => k.id !== target.id);
			flash('info', `Revoked "${target.name}".`);
		} catch (e) {
			flash('error', e instanceof ApiError ? e.message : 'failed to revoke key');
		} finally {
			revokePending = false;
		}
	}

	function appLabel(app_id: string | null): string {
		if (!app_id) return 'Instance-wide';
		const a = appBySlug.get(app_id);
		return a ? a.slug : app_id.slice(0, 8) + '…';
	}

	function shortDate(iso: string | null): string {
		if (!iso) return '—';
		return new Date(iso).toISOString().slice(0, 10);
	}

	function relative(iso: string | null): string {
		if (!iso) return 'Never';
		const then = new Date(iso).getTime();
		const sec = Math.round((Date.now() - then) / 1000);
		if (sec < 60) return `${sec}s ago`;
		const min = Math.round(sec / 60);
		if (min < 60) return `${min}m ago`;
		const hr = Math.round(min / 60);
		if (hr < 24) return `${hr}h ago`;
		const day = Math.round(hr / 24);
		if (day < 7) return `${day}d ago`;
		return shortDate(iso);
	}
</script>

{#if me}
	<section class="identity">
		<div class="identity-head">
			<h1>{me.username}</h1>
			<RoleChip role={me.instance_role} />
		</div>
		<dl class="identity-meta">
			<div>
				<dt>Email</dt>
				<dd>{me.email ?? 'No email set'}</dd>
			</div>
			<div>
				<dt>User ID</dt>
				<dd class="mono">{me.id}</dd>
			</div>
		</dl>
	</section>
{/if}

{#if deniedFromUsers}
	<div class="banner banner-info">
		You don't have access to the Users page. Ask an admin if you need to manage users.
	</div>
{/if}

{#if banner}
	<div class="banner banner-{banner.kind}">{banner.message}</div>
{/if}

<section class="keys-section">
	<header class="section-head">
		<h2>API keys</h2>
		{#if !mintOpen && !reveal}
			<button type="button" class="primary" onclick={openMint}>+ Mint API key</button>
		{/if}
	</header>

	{#if reveal}
		<div class="reveal">
			<h3>Save this token now — it will never be shown again.</h3>
			<p class="reveal-sub">
				Paste it into your CLI config or external integration. PiCloud only ever stores a hash; if
				you lose it, mint a new one.
			</p>
			<div class="token-row">
				<code class="token">{reveal.raw_token}</code>
				<button type="button" class="ghost" onclick={copyToken}>
					{copyState === 'copied' ? 'Copied ✓' : 'Copy'}
				</button>
			</div>
			<label class="ack">
				<input type="checkbox" bind:checked={revealAck} />
				<span>I've saved this token somewhere safe.</span>
			</label>
			<div class="reveal-actions">
				<button type="button" class="primary" disabled={!revealAck} onclick={dismissReveal}>
					Done
				</button>
			</div>
		</div>
	{/if}

	{#if mintOpen}
		<form class="mint" onsubmit={submitMint}>
			<div class="form-row">
				<label class="field">
					<span>Name</span>
					<input
						type="text"
						bind:value={mintForm.name}
						maxlength={NAME_MAX}
						autocomplete="off"
						placeholder="e.g. ci-deploy"
						required
					/>
					<small>1–{NAME_MAX} chars. Only you see it.</small>
				</label>

				<label class="field">
					<span>Binding</span>
					<select bind:value={mintForm.app_id}>
						<option value="">Instance-wide</option>
						{#each apps as a (a.id)}
							<option value={a.id}>{a.slug} ({a.name})</option>
						{/each}
					</select>
					<small>Pick an app to scope this key, or leave instance-wide.</small>
				</label>

				<label class="field">
					<span>Expires</span>
					<input type="date" bind:value={mintForm.expires_at} />
					<small>Leave blank for no expiry.</small>
				</label>
			</div>

			<fieldset class="scopes">
				<legend>Scopes</legend>
				<div class="scope-grid">
					{#each ALL_SCOPES as scope (scope)}
						{@const instanceScope = scopeIsInstance(scope)}
						{@const disabled = boundToApp && instanceScope}
						<label
							class="scope-chip"
							class:disabled
							title={disabled ? "Bound keys can't carry instance scopes" : undefined}
						>
							<input
								type="checkbox"
								checked={mintForm.scopes.has(scope)}
								disabled={disabled || mintPending}
								onchange={() => toggleScope(scope)}
							/>
							<span class="scope-name">{scope}</span>
						</label>
					{/each}
				</div>
				<small class="scope-hint">
					{mintForm.scopes.size === 0
						? 'Pick at least one scope.'
						: `${mintForm.scopes.size} scope${mintForm.scopes.size === 1 ? '' : 's'} selected.`}
				</small>
			</fieldset>

			{#if mintError}
				<div class="error">{mintError}</div>
			{/if}

			<div class="form-actions">
				<button type="button" class="ghost" onclick={cancelMint}>Cancel</button>
				<button type="submit" class="primary" disabled={!canSubmit}>
					{mintPending ? 'Minting…' : 'Mint key'}
				</button>
			</div>
		</form>
	{/if}

	{#if loadError}
		<div class="error">
			{loadError}
			<button type="button" class="retry" onclick={refreshKeys}>Retry</button>
		</div>
	{:else if keys.length === 0 && !reveal && !mintOpen}
		<p class="empty">
			No API keys yet. Mint one to authenticate the CLI or external integrations.
		</p>
	{:else if keys.length > 0}
		<div class="table">
			<div class="row head-row">
				<div>Name</div>
				<div>Prefix</div>
				<div>Scopes</div>
				<div>Binding</div>
				<div>Created</div>
				<div>Last used</div>
				<div>Expires</div>
				<div class="actions-col"></div>
			</div>
			{#each keys as key (key.id)}
				<div class="row">
					<div class="name-cell">{key.name}</div>
					<div class="mono prefix">pic_{key.prefix}…</div>
					<div class="scopes-cell">
						{#each key.scopes as s (s)}
							<span class="scope-pill">{s}</span>
						{/each}
					</div>
					<div>{appLabel(key.app_id)}</div>
					<div>{shortDate(key.created_at)}</div>
					<div title={key.last_used_at ?? ''}>{relative(key.last_used_at)}</div>
					<div>{key.expires_at ? shortDate(key.expires_at) : 'Never'}</div>
					<div class="actions-col">
						<button
							type="button"
							class="danger-link"
							onclick={() => openRevoke(key)}
							aria-label="Revoke {key.name}"
						>
							Revoke
						</button>
					</div>
				</div>
			{/each}
		</div>
	{/if}
</section>

{#if revokeTarget}
	<ConfirmModal
		title="Revoke API key?"
		variant="danger"
		confirmLabel="Revoke"
		busy={revokePending}
		busyLabel="Revoking…"
		onConfirm={confirmRevoke}
		onCancel={() => (revokeTarget = null)}
	>
		<p>
			Revoking <strong>{revokeTarget.name}</strong> (<code>{revokeTarget.prefix}</code>) takes
			effect immediately. Any CLI or integration using it will start returning <code>401</code>
			on the next request.
		</p>
		<p class="muted">This can't be undone — mint a new key if you need one again.</p>
	</ConfirmModal>
{/if}

<style>
	.identity {
		background: #0b1220;
		border: 1px solid #1e293b;
		border-radius: 0.5rem;
		padding: 1.25rem 1.5rem;
		margin-bottom: 1.5rem;
	}
	.identity-head {
		display: flex;
		align-items: center;
		gap: 0.75rem;
		margin-bottom: 0.75rem;
	}
	.identity h1 {
		margin: 0;
		font-size: 1.25rem;
		color: #e2e8f0;
	}
	.identity-meta {
		display: grid;
		grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
		gap: 0.75rem 1.5rem;
		margin: 0;
	}
	.identity-meta div {
		display: flex;
		flex-direction: column;
		gap: 0.15rem;
	}
	.identity-meta dt {
		color: #64748b;
		font-size: 0.7rem;
		text-transform: uppercase;
		letter-spacing: 0.05em;
	}
	.identity-meta dd {
		margin: 0;
		color: #cbd5e1;
		font-size: 0.9rem;
	}

	.banner {
		padding: 0.55rem 0.85rem;
		border-radius: 0.375rem;
		margin-bottom: 1rem;
		font-size: 0.85rem;
	}
	.banner-error {
		background: #450a0a;
		border: 1px solid #b91c1c;
		color: #fecaca;
	}
	.banner-info {
		background: #0c2a36;
		border: 1px solid #155e75;
		color: #a5f3fc;
	}

	.section-head {
		display: flex;
		align-items: center;
		justify-content: space-between;
		margin-bottom: 1rem;
	}
	.section-head h2 {
		margin: 0;
		font-size: 1.05rem;
		color: #e2e8f0;
	}

	.reveal {
		background: #0b1220;
		border: 1px solid #ca8a04;
		border-radius: 0.5rem;
		padding: 1.25rem 1.5rem;
		margin-bottom: 1.25rem;
		display: flex;
		flex-direction: column;
		gap: 0.75rem;
	}
	.reveal h3 {
		margin: 0;
		font-size: 0.95rem;
		color: #fbbf24;
	}
	.reveal-sub {
		margin: 0;
		color: #cbd5e1;
		font-size: 0.85rem;
		line-height: 1.4;
	}
	.token-row {
		display: flex;
		align-items: stretch;
		gap: 0.5rem;
	}
	.token {
		flex: 1;
		background: #020617;
		border: 1px solid #1e293b;
		border-radius: 0.375rem;
		padding: 0.6rem 0.75rem;
		color: #e2e8f0;
		font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
		font-size: 0.85rem;
		overflow-x: auto;
		white-space: nowrap;
	}
	.ack {
		display: flex;
		align-items: center;
		gap: 0.5rem;
		font-size: 0.85rem;
		color: #cbd5e1;
		cursor: pointer;
	}
	.reveal-actions {
		display: flex;
		justify-content: flex-end;
	}

	.mint {
		background: #0b1220;
		border: 1px solid #1e293b;
		border-radius: 0.5rem;
		padding: 1.25rem 1.5rem;
		margin-bottom: 1.5rem;
		display: flex;
		flex-direction: column;
		gap: 1rem;
	}
	.form-row {
		display: grid;
		grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
		gap: 1rem;
	}
	.field {
		display: flex;
		flex-direction: column;
		gap: 0.3rem;
		font-size: 0.85rem;
		color: #cbd5e1;
	}
	.field input,
	.field select {
		background: #0f172a;
		color: #e2e8f0;
		border: 1px solid #1e293b;
		border-radius: 0.375rem;
		padding: 0.5rem 0.7rem;
		font-size: 0.9rem;
	}
	.field input:focus,
	.field select:focus {
		outline: none;
		border-color: #38bdf8;
	}
	.field small {
		color: #64748b;
		font-size: 0.72rem;
	}

	.scopes {
		border: 1px solid #1e293b;
		border-radius: 0.375rem;
		padding: 0.75rem 0.85rem;
		color: #cbd5e1;
	}
	.scopes legend {
		font-size: 0.75rem;
		text-transform: uppercase;
		letter-spacing: 0.05em;
		color: #94a3b8;
		padding: 0 0.4rem;
	}
	.scope-grid {
		display: grid;
		grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
		gap: 0.4rem 0.75rem;
		margin-top: 0.25rem;
	}
	.scope-chip {
		display: flex;
		align-items: center;
		gap: 0.4rem;
		font-size: 0.85rem;
		font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
		cursor: pointer;
	}
	.scope-chip.disabled {
		opacity: 0.45;
		cursor: not-allowed;
	}
	.scope-hint {
		display: block;
		margin-top: 0.55rem;
		font-size: 0.75rem;
		color: #64748b;
	}

	.form-actions {
		display: flex;
		justify-content: flex-end;
		gap: 0.5rem;
	}

	.error {
		background: #450a0a;
		border: 1px solid #b91c1c;
		color: #fecaca;
		padding: 0.55rem 0.8rem;
		border-radius: 0.375rem;
		font-size: 0.85rem;
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 1rem;
		margin-top: 0.25rem;
	}
	.retry {
		background: transparent;
		border: 1px solid #b91c1c;
		color: #fecaca;
		padding: 0.2rem 0.55rem;
		border-radius: 0.25rem;
		cursor: pointer;
		font-size: 0.75rem;
	}

	.empty {
		color: #64748b;
		text-align: center;
		padding: 2.5rem 0;
		border: 1px dashed #1e293b;
		border-radius: 0.5rem;
	}

	.table {
		display: flex;
		flex-direction: column;
		border: 1px solid #1e293b;
		border-radius: 0.5rem;
		background: #0b1220;
		overflow: hidden;
	}
	.row {
		display: grid;
		grid-template-columns: 1.3fr 0.9fr 2fr 1fr 0.8fr 0.8fr 0.8fr 0.7fr;
		align-items: center;
		gap: 0.75rem;
		padding: 0.7rem 1rem;
		border-bottom: 1px solid #1e293b;
		font-size: 0.85rem;
	}
	.row:last-child {
		border-bottom: none;
	}
	.head-row {
		color: #94a3b8;
		font-size: 0.7rem;
		text-transform: uppercase;
		letter-spacing: 0.05em;
		background: #0f172a;
	}
	.name-cell {
		color: #e2e8f0;
		font-weight: 500;
	}
	.mono {
		font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	}
	.prefix {
		color: #94a3b8;
	}
	.scopes-cell {
		display: flex;
		flex-wrap: wrap;
		gap: 0.25rem;
	}
	.scope-pill {
		background: #1e293b;
		color: #cbd5e1;
		font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
		font-size: 0.7rem;
		padding: 0.1rem 0.4rem;
		border-radius: 0.25rem;
	}
	.actions-col {
		display: flex;
		justify-content: flex-end;
	}
	.danger-link {
		background: transparent;
		color: #fca5a5;
		border: none;
		font-size: 0.8rem;
		cursor: pointer;
		padding: 0.2rem 0.4rem;
		border-radius: 0.25rem;
	}
	.danger-link:hover {
		background: #450a0a;
		color: #fecaca;
	}

	button.primary {
		background: #38bdf8;
		color: #0b1220;
		border: none;
		padding: 0.5rem 0.9rem;
		border-radius: 0.375rem;
		font-weight: 600;
		cursor: pointer;
		font-size: 0.85rem;
	}
	button.primary:disabled {
		opacity: 0.6;
		cursor: not-allowed;
	}
	button.ghost {
		background: transparent;
		color: #94a3b8;
		border: 1px solid #334155;
		padding: 0.45rem 0.85rem;
		border-radius: 0.375rem;
		cursor: pointer;
		font-size: 0.85rem;
	}
	button.ghost:hover {
		background: #1e293b;
		color: #e2e8f0;
	}

	.muted {
		color: #94a3b8;
	}
</style>
