<!--
  OutgoingWebhookPanel

  Operator-facing controls for an outgoing webhook tier (global / project /
  stage / site). Three concerns in vertical flow:
    1. Signing state — secret reveal, copy, regenerate, disable.
    2. Send-test — fires a synthetic event and renders the receiver's
       response inline (status code, latency, body preview).
    3. Inheritance hint — when the parent didn't set a URL, shows that
       this tier will fall through to the next-most-general tier.

  Parent supplies the four async callbacks; the panel doesn't know which
  tier it lives on. Mirrors the inbound-webhook WebhookPanel ergonomics so
  operators see a consistent visual language for every webhook surface.
-->
<script lang="ts">
	import { onMount } from 'svelte';
	import { toasts } from '$lib/stores/toast';
	import { t } from '$lib/i18n';
	import {
		IconCopy, IconRefresh, IconLoader, IconKey, IconShield, IconCheck, IconAlert
	} from '$lib/components/icons';
	import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
	import type { NotificationSecretResponse, NotificationTestResult } from '$lib/api';

	interface Props {
		title: string;
		description: string;
		// True when *this tier* has a URL configured (passed in by the
		// parent that owns the URL field). Drives whether "send test" can
		// fire and whether we render the inheritance hint.
		hasUrl: boolean;
		// Optional human-readable name of the tier we'd fall through to if
		// the URL is empty (e.g. "global settings" when on a project page).
		fallbackLabel?: string;
		fetchSecret: () => Promise<NotificationSecretResponse>;
		regenerateSecret: () => Promise<NotificationSecretResponse>;
		disableSigning: () => Promise<NotificationSecretResponse>;
		sendTest: () => Promise<NotificationTestResult>;
	}

	let {
		title, description, hasUrl, fallbackLabel,
		fetchSecret, regenerateSecret, disableSigning, sendTest,
	}: Props = $props();

	// Initial load is on-demand: showing the secret is an explicit
	// operator action, not a passive read of the page. This keeps the
	// secret out of the network response unless the operator asks for it.
	let secret = $state('');
	let hasSecret = $state(false);
	let revealed = $state(false);

	let loading = $state(false);
	let regenerating = $state(false);
	let disabling = $state(false);
	let testing = $state(false);

	let confirmRegenerate = $state(false);
	let confirmDisable = $state(false);

	let testResult = $state<NotificationTestResult | null>(null);

	// Tier presence-check happens once on mount so the "signing on/off"
	// pill is correct without revealing the secret.
	async function loadStatus() {
		try {
			const res = await fetchSecret();
			hasSecret = res.has_secret;
			// Don't store the secret yet — only when revealed.
		} catch {
			// Silent — the panel still renders with hasSecret=false.
		}
	}

	async function handleReveal() {
		loading = true;
		try {
			const res = await fetchSecret();
			secret = res.secret;
			hasSecret = res.has_secret;
			revealed = true;
		} catch (err) {
			toasts.error(err instanceof Error ? err.message : $t('outgoingWebhook.loadFailed'));
		} finally {
			loading = false;
		}
	}

	async function handleRegenerate() {
		confirmRegenerate = false;
		regenerating = true;
		try {
			const res = await regenerateSecret();
			secret = res.secret;
			hasSecret = true;
			revealed = true;
			toasts.success($t('outgoingWebhook.regenerated'));
		} catch (err) {
			toasts.error(err instanceof Error ? err.message : $t('outgoingWebhook.regenerateFailed'));
		} finally {
			regenerating = false;
		}
	}

	async function handleDisable() {
		confirmDisable = false;
		disabling = true;
		try {
			await disableSigning();
			secret = '';
			hasSecret = false;
			revealed = false;
			toasts.info($t('outgoingWebhook.disabled'));
		} catch (err) {
			toasts.error(err instanceof Error ? err.message : $t('outgoingWebhook.disableFailed'));
		} finally {
			disabling = false;
		}
	}

	async function handleTest() {
		testing = true;
		testResult = null;
		try {
			testResult = await sendTest();
		} catch (err) {
			toasts.error(err instanceof Error ? err.message : $t('outgoingWebhook.testFailed'));
		} finally {
			testing = false;
		}
	}

	function handleCopy(value: string, successKey: string) {
		if (!value) return;
		navigator.clipboard.writeText(value).then(
			() => toasts.info($t(successKey)),
			() => toasts.error($t('outgoingWebhook.copyFailed')),
		);
	}

	// Coarse classification of the test result so the status pill picks
	// the right colour band: 2xx green, 4xx amber, 5xx / network red.
	function resultClass(r: NotificationTestResult): string {
		if (r.error && !r.status_code) return 'failure';
		if (r.status_code >= 200 && r.status_code < 300) return 'success';
		if (r.status_code >= 400 && r.status_code < 500) return 'warn';
		return 'failure';
	}

	onMount(() => {
		loadStatus();
	});
</script>

<div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-6 shadow-[var(--shadow-sm)]">
	<div class="mb-4 flex items-start justify-between gap-3">
		<div>
			<h2 class="mb-1 text-lg font-semibold text-[var(--text-primary)]">{title}</h2>
			<p class="text-sm text-[var(--text-secondary)]">{description}</p>
		</div>
		<!-- Signing-state pill: shown at all times so an operator can tell
		     "signed" vs "unsigned" without revealing the secret. -->
		{#if hasSecret}
			<span class="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-[var(--color-success-light)] px-2.5 py-1 text-xs font-medium text-[var(--color-success)]">
				<IconShield size={12} />
				{$t('outgoingWebhook.signingOn')}
			</span>
		{:else}
			<span class="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-[var(--surface-card-hover)] px-2.5 py-1 text-xs font-medium text-[var(--text-tertiary)]">
				<IconKey size={12} />
				{$t('outgoingWebhook.signingOff')}
			</span>
		{/if}
	</div>

	<!-- Inheritance hint: when this tier has no URL, real events fall
	     through to the parent tier. Surfacing this prevents the surprise
	     of "I configured the URL but events don't arrive". -->
	{#if !hasUrl}
		<div class="mb-4 flex items-start gap-2 rounded-lg border border-[var(--border-primary)] bg-[var(--surface-card-hover)] px-3 py-2.5 text-sm text-[var(--text-secondary)]">
			<IconAlert size={16} />
			<span>
				{#if fallbackLabel}
					{$t('outgoingWebhook.fallbackTo', { label: fallbackLabel })}
				{:else}
					{$t('outgoingWebhook.noUrlConfigured')}
				{/if}
			</span>
		</div>
	{/if}

	<!-- Secret control row.
	     - When unrevealed, render an opaque placeholder + a "Reveal" CTA
	       so the operator commits to the action of pulling the secret
	       on screen.
	     - When revealed, show the cleartext + copy. -->
	<div class="mb-2 text-xs font-medium uppercase tracking-wider text-[var(--text-tertiary)]">
		{$t('outgoingWebhook.signingSecret')}
	</div>
	<div class="flex items-stretch gap-2">
		<code class="flex-1 truncate rounded-lg border border-[var(--border-primary)] bg-[var(--surface-card-hover)] px-3 py-2.5 font-mono text-sm text-[var(--text-secondary)]">
			{#if revealed && secret}
				{secret}
			{:else if hasSecret}
				<span class="select-none text-[var(--text-tertiary)]">••••••••••••••••••••••••••••••••</span>
			{:else}
				<span class="select-none italic text-[var(--text-tertiary)]">{$t('outgoingWebhook.noSecret')}</span>
			{/if}
		</code>
		{#if revealed && secret}
			<button
				type="button"
				onclick={() => handleCopy(secret, 'outgoingWebhook.copied')}
				class="inline-flex items-center gap-1.5 rounded-lg border border-[var(--border-primary)] px-3 py-2.5 text-sm font-medium text-[var(--text-secondary)] transition-colors hover:bg-[var(--surface-card-hover)] active:animate-press"
			>
				<IconCopy size={16} />
				{$t('outgoingWebhook.copy')}
			</button>
		{:else}
			<button
				type="button"
				onclick={handleReveal}
				disabled={loading}
				class="inline-flex items-center gap-1.5 rounded-lg border border-[var(--border-primary)] px-3 py-2.5 text-sm font-medium text-[var(--text-secondary)] transition-colors hover:bg-[var(--surface-card-hover)] disabled:opacity-50 active:animate-press"
			>
				{#if loading}<IconLoader size={16} />{:else}<IconKey size={16} />{/if}
				{hasSecret ? $t('outgoingWebhook.reveal') : $t('outgoingWebhook.generate')}
			</button>
		{/if}
	</div>

	<!-- Action row: regenerate / disable. Both are destructive (regenerate
	     invalidates every receiver verifying the old secret; disable lets
	     subsequent events go out unsigned), so each opens a modal
	     ConfirmDialog rather than relying on a slim inline confirm strip
	     that's easy to miss next to the other buttons. -->
	<div class="mt-4 flex flex-wrap items-center gap-2">
		<button
			type="button"
			onclick={() => (confirmRegenerate = true)}
			disabled={regenerating}
			class="inline-flex items-center gap-2 rounded-lg border border-[var(--color-danger)] px-3 py-1.5 text-sm font-medium text-[var(--color-danger)] transition-colors hover:bg-[var(--color-danger-light)] disabled:opacity-50 active:animate-press"
		>
			{#if regenerating}<IconLoader size={14} />{:else}<IconRefresh size={14} />{/if}
			{$t('outgoingWebhook.regenerate')}
		</button>
		{#if hasSecret}
			<button
				type="button"
				onclick={() => (confirmDisable = true)}
				disabled={disabling}
				class="inline-flex items-center gap-2 rounded-lg border border-[var(--border-primary)] px-3 py-1.5 text-sm font-medium text-[var(--text-secondary)] transition-colors hover:bg-[var(--surface-card-hover)] disabled:opacity-50 active:animate-press"
			>
				{#if disabling}<IconLoader size={14} />{/if}
				{$t('outgoingWebhook.disable')}
			</button>
		{/if}
	</div>

	<!-- Send-test row. Disabled when no URL is configured *and* there's
	     no fallback (otherwise the resolver would surface the inherited
	     URL, and the test should reflect what a real event would do). -->
	<div class="mt-6 border-t border-[var(--border-primary)] pt-5">
		<div class="flex items-center justify-between gap-3">
			<div>
				<div class="text-sm font-semibold text-[var(--text-primary)]">{$t('outgoingWebhook.sendTestTitle')}</div>
				<p class="mt-0.5 text-xs text-[var(--text-tertiary)]">{$t('outgoingWebhook.sendTestHelp')}</p>
			</div>
			<button
				type="button"
				onclick={handleTest}
				disabled={testing}
				class="inline-flex items-center gap-2 rounded-lg bg-[var(--color-brand-600)] px-4 py-2 text-sm font-medium text-white shadow-sm transition-all duration-150 hover:bg-[var(--color-brand-700)] disabled:opacity-50 active:animate-press"
			>
				{#if testing}<IconLoader size={16} />{/if}
				{testing ? $t('outgoingWebhook.sending') : $t('outgoingWebhook.sendTest')}
			</button>
		</div>

		{#if testResult}
			{@const cls = resultClass(testResult)}
			<div
				class="mt-4 overflow-hidden rounded-lg border"
				class:border-[var(--color-success)]={cls === 'success'}
				class:border-[var(--color-warning)]={cls === 'warn'}
				class:border-[var(--color-danger)]={cls === 'failure'}
			>
				<!-- Result header: status pill + latency + signed badge -->
				<div class="flex flex-wrap items-center gap-3 px-4 py-3"
					class:bg-[var(--color-success-light)]={cls === 'success'}
					class:bg-[var(--color-warning-light)]={cls === 'warn'}
					class:bg-[var(--color-danger-light)]={cls === 'failure'}
				>
					<span
						class="inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 font-mono text-sm font-semibold"
						class:text-[var(--color-success)]={cls === 'success'}
						class:text-[var(--color-warning)]={cls === 'warn'}
						class:text-[var(--color-danger)]={cls === 'failure'}
					>
						{#if cls === 'success'}<IconCheck size={14} />{:else}<IconAlert size={14} />{/if}
						{testResult.status_code || $t('outgoingWebhook.networkError')}
					</span>
					<span class="text-xs text-[var(--text-secondary)]">
						{testResult.latency_ms}ms
					</span>
					<span class="text-xs text-[var(--text-tertiary)]">
						{$t('outgoingWebhook.tier')}: <span class="font-mono">{testResult.tier}</span>
					</span>
					{#if testResult.signature_sent}
						<span class="inline-flex items-center gap-1 text-xs text-[var(--text-secondary)]">
							<IconShield size={12} />
							{$t('outgoingWebhook.signed')}
						</span>
					{:else}
						<span class="text-xs text-[var(--text-tertiary)]">{$t('outgoingWebhook.unsigned')}</span>
					{/if}
				</div>

				<!-- Result body: delivery ID (so operators can grep their
				     receiver logs) + response preview if any. -->
				<div class="space-y-2 px-4 py-3">
					{#if testResult.error}
						<div class="text-sm text-[var(--color-danger)]">
							{testResult.error}
						</div>
					{/if}
					<div class="text-xs">
						<span class="text-[var(--text-tertiary)]">{$t('outgoingWebhook.deliveryId')}:</span>
						<code class="ml-1 font-mono text-[var(--text-secondary)]">{testResult.delivery_id}</code>
					</div>
					{#if testResult.response_snippet}
						<div>
							<div class="mb-1 text-xs font-medium uppercase tracking-wider text-[var(--text-tertiary)]">
								{$t('outgoingWebhook.responseBody')}
							</div>
							<pre class="max-h-40 overflow-auto rounded border border-[var(--border-primary)] bg-[var(--surface-card-hover)] p-2 font-mono text-xs text-[var(--text-secondary)] whitespace-pre-wrap">{testResult.response_snippet}</pre>
						</div>
					{/if}
				</div>
			</div>
		{/if}
	</div>
</div>

<!-- Modal confirms for both destructive actions. Mounted outside the panel
     card so backdrop + scale-in animation cover the page, not just the
     panel rectangle (matches how the rest of Tinyforge surfaces deletes). -->
<ConfirmDialog
	open={confirmRegenerate}
	title={$t('outgoingWebhook.confirmRegenerateTitle')}
	message={$t('outgoingWebhook.confirmRegenerate')}
	confirmLabel={$t('outgoingWebhook.regenerate')}
	confirmVariant="danger"
	onconfirm={handleRegenerate}
	oncancel={() => (confirmRegenerate = false)}
/>

<ConfirmDialog
	open={confirmDisable}
	title={$t('outgoingWebhook.confirmDisableTitle')}
	message={$t('outgoingWebhook.confirmDisable')}
	confirmLabel={$t('outgoingWebhook.disable')}
	confirmVariant="danger"
	onconfirm={handleDisable}
	oncancel={() => (confirmDisable = false)}
/>
