<script lang="ts">
    import { onMount } from 'svelte';
    import Modal from '$lib/components/Modal.svelte';
    import { ApiError } from '$lib/api/client';
    import {
        getCrawlerSettings,
        updateCrawlerSettings,
        getAnalysisSettings,
        updateAnalysisSettings,
        type CrawlerSettings,
        type CrawlerEnvOnly,
        type AnalysisSettings
    } from '$lib/api/admin';

    type Tab = 'crawler' | 'analysis';
    let tab = $state<Tab>('crawler');

    // Crawler state
    let crawler = $state<CrawlerSettings | null>(null);
    let crawlerEnv = $state<CrawlerEnvOnly | null>(null);
    let allowlistText = $state(''); // textarea mirror of download_allowlist

    // Analysis state. The full settings object is loaded and saved as-is;
    // vision-only fields (endpoint, model, prompts, slicing, sampling) ride
    // along unchanged while the OCR backend is active — only the OCR-relevant
    // knobs are surfaced below.
    let analysis = $state<AnalysisSettings | null>(null);

    let loading = $state(true);
    let loadError = $state<string | null>(null);
    let saving = $state(false);
    let saveError = $state<string | null>(null);
    let success = $state<string | null>(null);
    // field -> message, parsed from a 422 validation_failed response
    let fieldErrors = $state<Record<string, string>>({});
    let confirmOpen = $state(false);

    onMount(load);

    async function load() {
        loading = true;
        loadError = null;
        try {
            const [c, a] = await Promise.all([getCrawlerSettings(), getAnalysisSettings()]);
            crawler = c.editable;
            crawlerEnv = c.env_only;
            allowlistText = c.editable.download_allowlist.join('\n');
            analysis = a.editable;
        } catch (e) {
            loadError = e instanceof Error ? e.message : 'Failed to load settings.';
        } finally {
            loading = false;
        }
    }

    function parseFieldErrors(e: unknown): Record<string, string> {
        const out: Record<string, string> = {};
        if (
            e instanceof ApiError &&
            e.details &&
            typeof e.details === 'object' &&
            'fields' in e.details
        ) {
            const fields = (e.details as { fields?: { field: string; message: string }[] }).fields;
            for (const f of fields ?? []) out[f.field] = f.message;
        }
        return out;
    }

    function askSave() {
        saveError = null;
        success = null;
        fieldErrors = {};
        confirmOpen = true;
    }

    async function save() {
        confirmOpen = false;
        saving = true;
        saveError = null;
        success = null;
        fieldErrors = {};
        try {
            if (tab === 'crawler' && crawler) {
                // Sync the allowlist textarea back into the model.
                crawler.download_allowlist = allowlistText
                    .split('\n')
                    .map((s) => s.trim())
                    .filter(Boolean);
                const r = await updateCrawlerSettings(crawler);
                crawler = r.editable;
                crawlerEnv = r.env_only;
                allowlistText = r.editable.download_allowlist.join('\n');
            } else if (tab === 'analysis' && analysis) {
                const r = await updateAnalysisSettings(analysis);
                analysis = r.editable;
            }
            success = 'Saved and applied. The subsystem was restarted with the new settings.';
        } catch (e) {
            fieldErrors = parseFieldErrors(e);
            saveError =
                Object.keys(fieldErrors).length > 0
                    ? 'Some fields need fixing.'
                    : e instanceof Error
                      ? e.message
                      : 'Save failed.';
        } finally {
            saving = false;
        }
    }

</script>

<h1>Settings</h1>
<p class="lead muted">
    Edit crawler and analysis configuration. Saving applies immediately — the affected daemon is
    gracefully restarted, so in-flight jobs are retried rather than lost. Host-level and secret
    values stay environment-managed and are shown read-only.
</p>

<div class="tabs" role="tablist">
    <button
        role="tab"
        aria-selected={tab === 'crawler'}
        class:active={tab === 'crawler'}
        onclick={() => (tab = 'crawler')}
        data-testid="settings-tab-crawler">Crawler</button
    >
    <button
        role="tab"
        aria-selected={tab === 'analysis'}
        class:active={tab === 'analysis'}
        onclick={() => (tab = 'analysis')}
        data-testid="settings-tab-analysis">Analysis</button
    >
</div>

{#if loading}
    <p class="muted">Loading…</p>
{:else if loadError}
    <p class="error" role="alert">{loadError}</p>
{:else}
    <div class="sticky-bar">
        {#if tab === 'crawler' && crawler}
            <label class="toggle">
                <input
                    type="checkbox"
                    bind:checked={crawler.daemon_enabled}
                    data-testid="settings-crawler-enabled"
                />
                <span>Crawler daemon {crawler.daemon_enabled ? 'enabled' : 'disabled'}</span>
            </label>
        {:else if tab === 'analysis' && analysis}
            <label class="toggle">
                <input
                    type="checkbox"
                    bind:checked={analysis.enabled}
                    data-testid="settings-analysis-enabled"
                />
                <span>Analysis worker {analysis.enabled ? 'enabled' : 'disabled'}</span>
            </label>
        {/if}
        <div class="spacer"></div>
        <button class="primary" disabled={saving} onclick={askSave} data-testid="settings-save">
            {saving ? 'Saving…' : 'Save & apply'}
        </button>
    </div>

    {#if success}<p class="notice" role="status">{success}</p>{/if}
    {#if saveError}<p class="error" role="alert">{saveError}</p>{/if}

    {#if tab === 'crawler' && crawler}
        <div class="form">
            <fieldset>
                <legend>Schedule</legend>
                <label class="field">
                    <span>Daily metadata pass (HH:MM)</span>
                    <input type="time" bind:value={crawler.daily_at} />
                    {#if fieldErrors.daily_at}<small class="fe">{fieldErrors.daily_at}</small>{/if}
                </label>
                <label class="field">
                    <span>Timezone (IANA)</span>
                    <input type="text" bind:value={crawler.tz} placeholder="UTC" />
                    {#if fieldErrors.tz}<small class="fe">{fieldErrors.tz}</small>{/if}
                </label>
                <label class="field">
                    <span>Idle timeout (seconds)</span>
                    <input type="number" min="0" bind:value={crawler.idle_timeout_secs} />
                </label>
            </fieldset>

            <fieldset>
                <legend>Source</legend>
                <label class="field">
                    <span>Start URL</span>
                    <input
                        type="text"
                        bind:value={crawler.start_url}
                        placeholder="https://source.example/"
                    />
                    {#if fieldErrors.start_url}<small class="fe">{fieldErrors.start_url}</small>{/if}
                </label>
                <label class="field">
                    <span>CDN host</span>
                    <input type="text" bind:value={crawler.cdn_host} placeholder="cdn.example" />
                </label>
                <label class="field">
                    <span>Cookie domain</span>
                    <input
                        type="text"
                        bind:value={crawler.cookie_domain}
                        placeholder="source.example"
                    />
                </label>
                <label class="field">
                    <span>User-Agent</span>
                    <input type="text" bind:value={crawler.user_agent} />
                </label>
            </fieldset>

            <fieldset>
                <legend>Rate limiting</legend>
                <label class="field">
                    <span>Request interval (ms)</span>
                    <input type="number" min="0" bind:value={crawler.rate_ms} />
                </label>
                <label class="field">
                    <span>CDN request interval (ms)</span>
                    <input type="number" min="0" bind:value={crawler.cdn_rate_ms} />
                </label>
            </fieldset>

            <fieldset>
                <legend>Workers &amp; retention</legend>
                <label class="field">
                    <span>Chapter workers</span>
                    <input type="number" min="1" bind:value={crawler.chapter_workers} />
                    {#if fieldErrors.chapter_workers}<small class="fe"
                            >{fieldErrors.chapter_workers}</small
                        >{/if}
                </label>
                <label class="field">
                    <span>Job retention (days)</span>
                    <input type="number" min="0" bind:value={crawler.retention_days} />
                </label>
                <label class="field">
                    <span>Manga limit per pass (0 = unlimited)</span>
                    <input type="number" min="0" bind:value={crawler.manga_limit} />
                </label>
            </fieldset>

            <fieldset>
                <legend>Download safety</legend>
                <label class="field-inline">
                    <input type="checkbox" bind:checked={crawler.allow_any_host} />
                    <span>Allow any host (bypass allowlist; still blocks private IPs)</span>
                </label>
                <label class="field">
                    <span>Download allowlist (one host per line)</span>
                    <textarea
                        rows="4"
                        bind:value={allowlistText}
                        disabled={crawler.allow_any_host}
                    ></textarea>
                </label>
                <label class="field">
                    <span>Max image bytes</span>
                    <input type="number" min="0" bind:value={crawler.max_image_bytes} />
                </label>
            </fieldset>

            <fieldset>
                <legend>Reliability</legend>
                <label class="field">
                    <span>Chapter job timeout (seconds)</span>
                    <input type="number" min="1" bind:value={crawler.job_timeout_secs} />
                    {#if fieldErrors.job_timeout_secs}<small class="fe"
                            >{fieldErrors.job_timeout_secs}</small
                        >{/if}
                </label>
                <label class="field">
                    <span>Metadata max consecutive failures</span>
                    <input
                        type="number"
                        min="0"
                        bind:value={crawler.metadata_max_consecutive_failures}
                    />
                </label>
                <label class="field">
                    <span>Browser restart threshold</span>
                    <input type="number" min="0" bind:value={crawler.browser_restart_threshold} />
                </label>
            </fieldset>

            {#if crawlerEnv}
                <fieldset class="env">
                    <legend>Managed via environment (read-only)</legend>
                    <dl>
                        <dt>Browser mode</dt>
                        <dd>{crawlerEnv.browser_mode}</dd>
                        <dt>Browser args</dt>
                        <dd>{crawlerEnv.browser_args.join(' ') || '—'}</dd>
                        <dt>Proxy</dt>
                        <dd>{crawlerEnv.proxy ?? '—'}</dd>
                        <dt>TOR control URL</dt>
                        <dd>{crawlerEnv.tor_control_url ?? '—'}</dd>
                        <dt>TOR credentials</dt>
                        <dd>{crawlerEnv.tor_credentials_configured ? 'configured' : '—'}</dd>
                        <dt>Initial session</dt>
                        <dd>{crawlerEnv.session_configured ? 'configured' : '—'}</dd>
                    </dl>
                </fieldset>
            {/if}
        </div>
    {:else if tab === 'analysis' && analysis}
        <div class="form">
            <fieldset>
                <legend>Engine</legend>
                <p class="env-note muted">
                    Backend: OCR (ocrs) — in-process text extraction. Selected via
                    environment; vision is temporarily disabled.
                </p>
            </fieldset>

            <fieldset>
                <legend>Worker</legend>
                <label class="field">
                    <span>Workers</span>
                    <input type="number" min="1" bind:value={analysis.workers} />
                    {#if fieldErrors.workers}<small class="fe">{fieldErrors.workers}</small>{/if}
                </label>
                <label class="field">
                    <span>Job timeout (seconds)</span>
                    <input type="number" min="1" bind:value={analysis.job_timeout_secs} />
                </label>
                <label class="field">
                    <span>Max image bytes</span>
                    <input type="number" min="0" bind:value={analysis.max_image_bytes} />
                </label>
            </fieldset>
        </div>
    {/if}
{/if}

<Modal
    open={confirmOpen}
    title="Apply settings?"
    onClose={() => (confirmOpen = false)}
    size="sm"
    closeOnBackdrop
    testid="settings-confirm"
>
    <p>
        Saving restarts the {tab} subsystem with the new settings. In-flight jobs are leased and
        will be retried, but any work in progress is interrupted briefly.
    </p>
    {#snippet footer()}
        <button type="button" onclick={() => (confirmOpen = false)}>Cancel</button>
        <button type="button" class="primary" onclick={save} data-testid="settings-confirm-apply">
            Save &amp; apply
        </button>
    {/snippet}
</Modal>

<style>
    h1 {
        margin: 0 0 var(--space-2) 0;
    }
    .lead {
        margin: 0 0 var(--space-4) 0;
        font-size: var(--font-sm);
        max-width: 60ch;
    }
    .muted {
        color: var(--text-muted);
    }
    .tabs {
        display: flex;
        gap: var(--space-2);
        margin-bottom: var(--space-4);
        border-bottom: 1px solid var(--border);
    }
    .tabs button {
        padding: var(--space-2) var(--space-3);
        background: none;
        border: none;
        border-bottom: 2px solid transparent;
        color: var(--text-muted);
        cursor: pointer;
        font-size: var(--font-sm);
    }
    .tabs button.active {
        color: var(--text);
        border-bottom-color: var(--primary);
        font-weight: var(--weight-semibold);
    }
    .sticky-bar {
        position: sticky;
        top: calc(var(--app-header-h) + var(--space-2));
        z-index: 1;
        display: flex;
        align-items: center;
        gap: var(--space-3);
        padding: var(--space-3);
        margin-bottom: var(--space-3);
        background: var(--surface-elevated);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
    }
    .sticky-bar .spacer {
        flex: 1;
    }
    .toggle {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2);
        font-size: var(--font-sm);
    }
    .form {
        display: grid;
        gap: var(--space-4);
    }
    fieldset {
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        padding: var(--space-3);
        display: grid;
        gap: var(--space-3);
    }
    legend {
        padding: 0 var(--space-2);
        font-weight: var(--weight-semibold);
        font-size: var(--font-sm);
    }
    .field {
        display: grid;
        gap: var(--space-1);
    }
    .field > span {
        font-size: var(--font-sm);
        color: var(--text-muted);
    }
    .field input,
    .field textarea {
        padding: var(--space-2) var(--space-3);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        background: var(--surface);
        color: var(--text);
        width: 100%;
        box-sizing: border-box;
    }
    .field-inline {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2);
        font-size: var(--font-sm);
    }
    .fe {
        color: var(--danger, #dc2626);
        font-size: var(--font-xs);
    }
    .env dl {
        display: grid;
        grid-template-columns: max-content 1fr;
        gap: var(--space-1) var(--space-3);
        margin: 0;
        font-size: var(--font-sm);
    }
    .env dt {
        color: var(--text-muted);
    }
    .env dd {
        margin: 0;
        word-break: break-all;
    }
    .env-note {
        font-size: var(--font-xs);
        margin: 0;
    }
    .error {
        color: var(--danger, #dc2626);
        padding: var(--space-2) var(--space-3);
        border: 1px solid var(--danger, #dc2626);
        border-radius: var(--radius-md);
        margin-bottom: var(--space-3);
    }
    .notice {
        color: var(--success, #16a34a);
        padding: var(--space-2) var(--space-3);
        border: 1px solid var(--success, #16a34a);
        border-radius: var(--radius-md);
        margin-bottom: var(--space-3);
    }
    button.primary {
        padding: var(--space-2) var(--space-3);
        border-radius: var(--radius-md);
        border: 1px solid var(--primary);
        background: var(--primary);
        color: white;
        cursor: pointer;
    }
    button.primary:disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }
</style>
