<script lang="ts">
    import {
        Standards,
        StandardsParamsSchema,
        StandardsDataSchema,
        type StandardsParams,
        type StandardsData,
    } from "@blade-and-brawn/calculator";
    import {
        Activity,
        Gender,
        lbToKg,
        type Metrics,
    } from "@blade-and-brawn/domain";
    import StandardsTable from "$lib/components/StandardsTable.svelte";
    import PlayersTable from "$lib/components/PlayersTable.svelte";
    import PercentInput from "$lib/components/PercentInput.svelte";
    import { api } from "$lib/api";
    import { Value } from "@sinclair/typebox/value";
    import { onMount } from "svelte";

    type ConfigSummary = { id: string; name: string };
    type DatasetSummary = { id: string; name: string };
    type Editor = {
        configId: string | null;
        name: string;
        datasetId: string;
        params: StandardsParams;
        data: StandardsData;
    };
    type Snapshot = {
        name: string;
        datasetId: string;
        params: StandardsParams;
    };

    const DRAFT_KEY = "calculator-draft";

    let activeTab = $state("standards");
    let activity = $state(Activity.BenchPress);
    let metrics = $state({
        gender: Gender.Male,
        age: null as number | null,
        weight: null as number | null, // lb
    });
    let configs = $state<ConfigSummary[]>([]);
    let datasets = $state<DatasetSummary[]>([]);
    let liveConfigId = $state<string | null>(null);

    let editor = $state<Editor | null>(null);
    let savedSnapshot = $state<Snapshot | null>(null);

    let loadError = $state<string | null>(null);
    let saveError = $state<string | null>(null);
    let saving = $state(false);
    let settingLive = $state(false);

    const allStandards = $derived(
        editor
            ? new Standards(
                  $state.snapshot({ data: editor.data, params: editor.params }),
              )
            : null,
    );

    const isDirty = $derived.by(() => {
        if (!editor || !savedSnapshot) return false;
        const current: Snapshot = {
            name: editor.name,
            datasetId: editor.datasetId,
            params: editor.params,
        };
        return JSON.stringify(current) !== JSON.stringify(savedSnapshot);
    });

    const selectedMetrics = $derived({
        gender: metrics.gender,
        age: metrics.age ?? 0,
        weight: lbToKg(metrics.weight ?? 0),
    } as Metrics);

    function errorMessage(err: unknown): string {
        const value = (err as { value?: { error?: string } })?.value;
        return (
            value?.error ??
            (err instanceof Error ? err.message : "Unknown error")
        );
    }

    function saveDraft() {
        if (!editor) return;
        try {
            localStorage.setItem(DRAFT_KEY, JSON.stringify(editor));
        } catch (err) {
            console.error("failed to save draft", err);
        }
    }

    async function refreshLists() {
        const [configsRes, datasetsRes] = await Promise.all([
            api.standards.configs.get(),
            api.standards.datasets.get(),
        ]);
        if (configsRes.error) throw configsRes.error;
        if (datasetsRes.error) throw datasetsRes.error;
        configs = configsRes.data;
        datasets = datasetsRes.data;
    }

    async function loadConfig(
        id: string,
        opt: { skipDirtyCheck?: boolean } = {},
    ) {
        if (
            !opt.skipDirtyCheck &&
            isDirty &&
            !confirm("Discard unsaved changes?")
        )
            return;

        saveError = null;
        const res = await api.standards.configs({ id }).get();
        if (res.error) {
            saveError = errorMessage(res.error);
            return;
        }
        Value.Assert(StandardsParamsSchema, res.data.params);
        Value.Assert(StandardsDataSchema, res.data.data);

        editor = {
            configId: res.data.id,
            name: res.data.name,
            datasetId: res.data.datasetId,
            params: res.data.params,
            data: res.data.data,
        };
        savedSnapshot = {
            name: res.data.name,
            datasetId: res.data.datasetId,
            params: structuredClone(res.data.params),
        };
        saveDraft();
    }

    async function onDatasetChange(datasetId: string) {
        if (!editor) return;
        const target = editor;
        saveError = null;
        const res = await api.standards.datasets({ id: datasetId }).get();
        if (res.error) {
            saveError = errorMessage(res.error);
            return;
        }
        Value.Assert(StandardsDataSchema, res.data.data);
        // the user may have switched to a different config while this was in flight
        if (editor !== target) return;
        editor.datasetId = datasetId;
        editor.data = res.data.data;
    }

    async function persist(mode: "update" | "create") {
        if (!editor || !editor.name) return;
        const target = editor;
        saveError = null;
        saving = true;
        try {
            if (mode === "update" && target.configId) {
                const res = await api.standards.configs({ id: target.configId }).put({
                    name: target.name,
                    datasetId: target.datasetId,
                    params: target.params,
                });
                if (res.error) throw res.error;
            } else {
                const res = await api.standards.configs.post({
                    name: target.name,
                    datasetId: target.datasetId,
                    params: target.params,
                });
                if (res.error) throw res.error;
                // only stamp the new id if the user hasn't switched away in the meantime
                if (editor === target) editor.configId = res.data.id;
            }
            if (editor === target) {
                savedSnapshot = {
                    name: target.name,
                    datasetId: target.datasetId,
                    params: $state.snapshot(target.params),
                };
                saveDraft();
            }
            await refreshLists();
        } catch (err) {
            saveError = errorMessage(err);
        } finally {
            saving = false;
        }
    }

    function save() {
        return persist(editor?.configId ? "update" : "create");
    }

    function saveAsNew() {
        return persist("create");
    }

    async function discard() {
        if (!editor?.configId) return;
        await loadConfig(editor.configId, { skipDirtyCheck: true });
    }

    async function setLive() {
        if (!editor?.configId || isDirty) return;
        const configId = editor.configId;
        saveError = null;
        settingLive = true;
        try {
            const res = await api.calculator.standards.config.switch.post({
                standardsConfigId: configId,
            });
            if (res.error) throw res.error;
            liveConfigId = configId;
        } catch (err) {
            saveError = errorMessage(err);
        } finally {
            settingLive = false;
        }
    }

    async function restoreDraft(): Promise<boolean> {
        const draftStringified = localStorage.getItem(DRAFT_KEY);
        if (!draftStringified) return false;

        try {
            const draft = JSON.parse(draftStringified) as Editor;
            Value.Assert(StandardsParamsSchema, draft.params);
            Value.Assert(StandardsDataSchema, draft.data);

            if (draft.configId) {
                const configRes = await api.standards.configs({ id: draft.configId }).get();
                if (configRes.error) throw configRes.error;
                savedSnapshot = {
                    name: configRes.data.name,
                    datasetId: configRes.data.datasetId,
                    params: configRes.data.params,
                };
            }

            editor = draft;
            return true;
        } catch {
            // stale/incompatible/dangling draft (schema drift, deleted config, corrupted JSON) - drop it
            localStorage.removeItem(DRAFT_KEY);
            saveError =
                "Your saved draft could not be restored, so the live config was loaded instead.";
            return false;
        }
    }

    onMount(async () => {
        try {
            const [, liveRes] = await Promise.all([
                refreshLists(),
                api.calculator.standards.config.get(),
            ]);
            if (liveRes.error) throw liveRes.error;
            Value.Assert(StandardsParamsSchema, liveRes.data.params);
            Value.Assert(StandardsDataSchema, liveRes.data.data);
            liveConfigId = liveRes.data.id;

            if (!(await restoreDraft())) {
                await loadConfig(liveRes.data.id, { skipDirtyCheck: true });
            }
        } catch (err) {
            loadError = errorMessage(err);
        }
    });

    $effect(() => {
        if (editor) saveDraft();
    });
</script>

{#if !(editor && allStandards)}
    <div class="w-full flex justify-center pt-10">
        {#if loadError}
            <div role="alert" class="alert alert-error max-w-lg">
                <span>Failed to load the calculator: {loadError}</span>
            </div>
        {:else}
            <span class="loading loading-spinner loading-lg"></span>
        {/if}
    </div>
{:else}
    {@const currentEditor = editor}
    {@const activityParams = currentEditor.params.activity[activity]}
    {@const activityMetadata = allStandards.byActivity(activity).getMetadata()}
    <div
        class="w-full -mt-10 -mx-10 px-6 py-2 bg-base-200 border-b border-base-300 flex items-center gap-2 flex-wrap text-sm"
    >
        <span class="label">Configuration</span>
        <select
            class="select select-sm"
            value={currentEditor.configId ?? ""}
            onchange={(e) => loadConfig(e.currentTarget.value)}
        >
            {#each configs as config}
                <option value={config.id}>
                    {config.name || "(untitled)"}{config.id === liveConfigId
                        ? " (live)"
                        : ""}
                </option>
            {/each}
        </select>

        {#if isDirty}
            <span class="badge badge-warning badge-sm">Unsaved</span>
        {:else if currentEditor.configId === liveConfigId}
            <span class="badge badge-success badge-sm">Live</span>
        {/if}

        <details class="dropdown">
            <summary
                class="btn btn-ghost btn-sm gap-1 font-normal opacity-70 list-none [&::-webkit-details-marker]:hidden"
            >
                More ▾
            </summary>
            <div
                class="dropdown-content z-10 menu bg-base-100 rounded-box shadow p-4 gap-3 w-64 mt-2"
            >
                <label class="w-full">
                    <span class="label mb-1">Configuration name</span>
                    <input
                        class="input input-sm w-full"
                        bind:value={currentEditor.name}
                        placeholder="Config name"
                    />
                </label>
                <label class="w-full">
                    <span class="label mb-1">Dataset</span>
                    <select
                        class="select select-sm w-full"
                        value={currentEditor.datasetId}
                        onchange={(e) => onDatasetChange(e.currentTarget.value)}
                    >
                        {#each datasets as dataset}
                            <option value={dataset.id}>{dataset.name}</option>
                        {/each}
                    </select>
                </label>
            </div>
        </details>

        <div class="flex-1"></div>

        <button
            class="btn btn-ghost btn-sm"
            disabled={!isDirty}
            onclick={discard}
        >
            Discard
        </button>
        <button
            class="btn btn-outline btn-sm"
            disabled={settingLive ||
                isDirty ||
                currentEditor.configId === liveConfigId}
            onclick={setLive}
        >
            {settingLive ? "Setting live..." : "Set as live"}
        </button>
        <button
            class="btn btn-outline btn-sm"
            disabled={saving || !currentEditor.name}
            onclick={saveAsNew}
        >
            Save as new
        </button>
        <button
            class="btn btn-primary btn-sm"
            disabled={saving || !isDirty}
            onclick={save}
        >
            {saving ? "Saving..." : "Save"}
        </button>
    </div>

    {#if saveError}
        <div role="alert" class="alert alert-error w-full mt-4">
            <span>{saveError}</span>
        </div>
    {/if}

    <section class="w-full flex gap-4 flex-wrap mt-6">
        <fieldset class="fieldset bg-base-200 p-4">
            <legend class="fieldset-legend text-lg font-semibold"
                >Global Parameters</legend
            >

            <label class="flex flex-col gap-1">
                <span class="label text-xs">Max level</span>
                <input
                    class="input input-sm w-36"
                    type="number"
                    min="1"
                    max="100"
                    placeholder="5"
                    bind:value={currentEditor.params.global.maxLevel}
                />
            </label>
        </fieldset>

        <fieldset class="fieldset bg-base-200 p-4">
            <legend class="fieldset-legend text-lg font-semibold"
                >{activityMetadata.name} Parameters</legend
            >

            <label class="flex flex-col gap-1 mb-3">
                <span class="label text-xs">Activity</span>
                <select
                    class="select select-sm w-36"
                    name="activities"
                    bind:value={activity}
                >
                    {#each Object.values(Activity) as a}
                        <option value={a}>
                            {allStandards.byActivity(a).getMetadata().name}
                        </option>
                    {/each}
                </select>
            </label>

            <div class="flex gap-3 flex-wrap">
                <fieldset class="fieldset bg-base-300 p-2">
                    <legend class="fieldset-legend text-sm font-semibold"
                        >Modifiers</legend
                    >

                    <PercentInput
                        label="Difficulty (%)"
                        min={-100}
                        max={100}
                        step={5}
                        value={activityParams.difficultyModifier}
                        onchange={(v) => (activityParams.difficultyModifier = v)}
                    />
                </fieldset>

                <fieldset class="fieldset bg-base-300 p-2">
                    <legend class="fieldset-legend text-sm font-semibold"
                        >Generation</legend
                    >

                    <div class="flex gap-3">
                        <label class="flex flex-col gap-1">
                            <span class="label text-xs">Enabled</span>
                            <input
                                type="checkbox"
                                bind:checked={activityParams.enableGeneration}
                                class="toggle toggle-sm self-start mt-1.5"
                            />
                        </label>
                        <PercentInput
                            label="Stretch lower (%)"
                            scale={10}
                            min={0}
                            max={100}
                            step={10}
                            value={activityParams.stretch.lower}
                            onchange={(v) => (activityParams.stretch.lower = v)}
                            disabled={!activityParams.enableGeneration}
                        />
                        <PercentInput
                            label="Stretch upper (%)"
                            scale={10}
                            min={0}
                            max={100}
                            step={10}
                            value={activityParams.stretch.upper}
                            onchange={(v) => (activityParams.stretch.upper = v)}
                            disabled={!activityParams.enableGeneration}
                        />
                        {#if activityMetadata.generators.some((g) => g.metric === "weight")}
                            <PercentInput
                                label="Weight sig. (%)"
                                min={-100}
                                max={100}
                                step={5}
                                value={activityParams.weightModifier}
                                onchange={(v) => (activityParams.weightModifier = v)}
                                disabled={!activityParams.enableGeneration}
                            />
                        {/if}
                        {#if activityMetadata.generators.some((g) => g.metric === "age")}
                            <PercentInput
                                label="Age sig. (%)"
                                min={-100}
                                max={100}
                                step={5}
                                value={activityParams.ageModifier}
                                onchange={(v) => (activityParams.ageModifier = v)}
                                disabled={!activityParams.enableGeneration}
                            />
                            <label class="flex flex-col gap-1">
                                <span class="label text-xs">Peak age</span>
                                <input
                                    class="input input-sm w-20"
                                    type="number"
                                    min="1"
                                    max="100"
                                    step="1"
                                    bind:value={activityParams.peakAge}
                                    disabled={!activityParams.enableGeneration}
                                />
                            </label>
                        {/if}
                    </div>
                </fieldset>
            </div>
        </fieldset>
    </section>

    <div class="divider"></div>

    <div role="tablist" class="tabs tabs-border">
        <button
            role="tab"
            class="tab"
            class:tab-active={activeTab === "standards"}
            onclick={() => (activeTab = "standards")}
        >
            Standards
        </button>

        <button
            role="tab"
            class="tab"
            class:tab-active={activeTab === "players"}
            onclick={() => (activeTab = "players")}
        >
            Players
        </button>
    </div>

    {#if activeTab === "standards"}
        <div class="flex items-center gap-4 text-sm opacity-70 mt-6">
            <span>Filter:</span>
            <label class="flex items-center gap-2">
                <span class="label">Gender</span>
                <select
                    class="select select-sm w-28"
                    name="genders"
                    bind:value={metrics.gender}
                >
                    {#each Object.values(Gender) as gender}
                        <option value={gender}>{gender}</option>
                    {/each}
                </select>
            </label>
            <label class="flex items-center gap-2">
                <span class="label">Age</span>
                <input
                    class="input input-sm w-20"
                    type="number"
                    min="0"
                    max="100"
                    placeholder="18"
                    bind:value={metrics.age}
                />
            </label>
            <label class="flex items-center gap-2">
                <span class="label">Weight (lb)</span>
                <input
                    class="input input-sm w-20"
                    type="number"
                    min="0"
                    max="600"
                    placeholder="170"
                    bind:value={metrics.weight}
                />
            </label>
        </div>

        <StandardsTable
            selected={{
                activity,
                metrics: selectedMetrics,
                standardsConfig: {
                    data: currentEditor.data,
                    params: currentEditor.params,
                },
            }}
            {allStandards}
        />
    {/if}

    {#if activeTab === "players"}
        <PlayersTable {allStandards} />
    {/if}
{/if}
