<!-- 
@component
Main component for rendering match scouting fields

Used in both the admin and regular pages.

TODO: Normalize the interface to use for game pieces

Props:
    - `season_uuid` (`string`) - The uuid of the season to render fields for
    - `year` (`number`) - The year to render fields for
    - `event_data` (`Event`) - The current event that is being scouted
    - `editable` (`boolean`) - If the fields are editable or not
-->
<script lang="ts">
	import { onMount } from "svelte";
	import { flip } from "svelte/animate";
	import { toast } from "svelte-sonner";
	import { dragHandleZone } from "svelte-dnd-action";
	import { CircleNotchIcon, DownloadSimpleIcon, ExportIcon, FolderPlusIcon, InfoIcon, PlusCircleIcon, TrashIcon, WarningIcon, XCircleIcon } from "phosphor-svelte";

	import Skeleton from "../ui/skeleton/skeleton.svelte";
    import * as Card from "$lib/components/ui/card/index.js";
    import * as Dialog from "$lib/components/ui/dialog/index.js";
	import Button from "../ui/button/button.svelte";
    import * as Select from "$lib/components/ui/select/index.js";
	import Separator from "../ui/separator/separator.svelte";
	import Input from "../ui/input/input.svelte";
	import * as Alert from "../ui/alert/index.js";

	import { db, type Event as EventType, type SeasonGamePiece, type SeasonMatchScoutingField } from "$lib/utils/db";
	import { getUser } from "$lib/utils/user";
	import { pushMatchScoutingData } from "$lib/utils/sync";
    import { addFieldDialogOpen, addSectionDialogOpen } from "$lib/stores/dialog";
    
	import { clearSeasonFieldsFieldsSeasonSeasonUuidClearDelete, createSeasonFieldFieldsSeasonSeasonUuidCreatePost, getMatchScoutingFieldPresetsFieldsGetPresetsGet, getSeasonFieldsFieldsSeasonSeasonUuidGet, moveMatchScoutingFieldsFieldsSeasonUuidReorderPatch } from "$lib/api/match-scouting-fields/match-scouting-fields";
	import { getSeasonGamepiecesGamepiecesSeasonSeasonUuidGet } from "$lib/api/gamepieces/gamepieces";
	import type { GamepieceResponse, MatchScoutingPresetResponse, MatchScoutingSeasonFieldsResponse, UserResponse } from "$lib/api/model";
    
	import StringField from "./fields/StringField.svelte";
	import LargeNumberField from "./fields/LargeNumberField.svelte";
	import SmallNumberField from "./fields/SmallNumberField.svelte";
	import BooleanField from "./fields/BooleanField.svelte";
	import ChoiceField from "./fields/ChoiceField.svelte";
	import MultipleChoiceField from "./fields/MultipleChoiceField.svelte";
	import Section from "./fields/Section.svelte";

	import AddSectionDialog from "./dialogs/AddSectionDialog.svelte";
	import AddFieldDialog from "./dialogs/AddFieldDialog.svelte";
	import MatchScoutingSubmit from "./MatchScoutingSubmit.svelte";
	import MatchScoutingTeamInfo from "./MatchScoutingMatchInfo.svelte";
	import CoarseSmallNumberField from "./fields/CoarseSmallNumberField.svelte";
        
    
    interface Props {
        season_uuid: string
        year: number
        event_data?: EventType | null
        editable: boolean
    }
    let { season_uuid, year, event_data = null, editable }: Props = $props();

    let fields: SeasonMatchScoutingField[] | MatchScoutingSeasonFieldsResponse[] = $state([]);
    let gamePieces: SeasonGamePiece[] | GamepieceResponse[] = $state([]);
    
    let user: UserResponse | null = getUser();
    let matchScoutingTeamInfoChild: ReturnType<typeof MatchScoutingTeamInfo>;

    let fieldFile = $state(null);
    let presets: MatchScoutingPresetResponse[] = $state([]);
    let selectedPresetName = $state(null);
    let selectedPreset = $derived(
        presets.find(p => p.name === selectedPresetName) ?? null
    );
    
    /**
     * Get the structure of fields and sections from the server if editing, and from the offline cache otherwise
     */
    async function getStructure(): Promise<void> {
        if (editable) {
            await getSeasonFieldsFieldsSeasonSeasonUuidGet(season_uuid).then((response) => {
                if (response.status === 200) {
                    fields = response.data;
                } else {
                    console.warn("Failed to fetch match scouting fields from the server");
                    toast.warning("Failed to fetch match scouting fields from the server", { duration: 5000 });
                }
            })
        } else {
            const season = await db.season_data.get(season_uuid);
            if (season) {
                fields = season.fields;
            } else {
                console.warn("Failed to fetch match scouting fields from the local database");
                toast.warning("Failed to fetch match scouting fields from the local database", { duration: 5000 });
            }
        }

        if (fields) {
            fields = normalizeOrders(fields);
        }
    }

    /**
     * Get game pieces from the server if editing, and from the offline cache otherwise
     */
    async function getGamePieces(): Promise<void> {
        if (editable) {
            await getSeasonGamepiecesGamepiecesSeasonSeasonUuidGet(season_uuid).then((response) => {
                if (response.status === 200) {
                    gamePieces = response.data
                }
            });
        } else {
            const season = await db.season_data.get(season_uuid);
            if (season?.game_pieces) {
                gamePieces = season?.game_pieces
            } else {
                console.warn("Could not get game pieces from season data");
            }
        }
    }

    /**
     * Submit match scouting data to the local cache
     * 
     * Then, attempt to push it to the server
     * 
     * @param event Click event
     */
    async function submit(event: Event): Promise<void> {
        event.preventDefault();

        const form = event.currentTarget as HTMLFormElement;
        const formData = new FormData(form);

        const EXCLUDED_KEYS = new Set([
            "team_number",
            "match_number",
            "match_type",
            "position",
        ]);

        // Exclude fields and merge duplicates (likely from a multiple_choice field) into a stringified array
        const filteredFields: Record<string, string> = {};
        const seen = new Set<string>();

        for (const [key, value] of formData.entries()) {
            if (EXCLUDED_KEYS.has(key)) continue;

            if (seen.has(key)) {
                const current = filteredFields[key];
                const arr = current.startsWith("[")
                    ? JSON.parse(current)
                    : [current];

                arr.push(value);
                filteredFields[key] = JSON.stringify(arr);
            } else if (value === "") {
                filteredFields[key] = "";
            } else {
                seen.add(key);
                filteredFields[key] = value as string;
            }
        }

        await db.match_scouting.add({
            uuid: crypto.randomUUID(),
            data: filteredFields,
            user_uuid: user?.uuid ?? "",
            year: event_data.year,
            team_number: parseInt(formData.get("team_number")),
            match_number: parseInt(formData.get("match_number")),
            match_type: formData.get("match_type"),
            event_code: event_data.event_code,
            event_name: event_data.name,
            event_type: event_data.type,
            event_city: event_data.city,
            event_country: event_data.country,
            event_start_date: event_data.start_date,
            event_end_date: event_data.end_date,
            synced: false
        });

        toast.success("Match scouting data saved locally", { duration: 5000 });
        form.reset();

        // Delay 100ms while form is resetting
        setTimeout(() => {
            matchScoutingTeamInfoChild.increment_match_number(parseInt(formData.get("match_number")), formData.get("match_type"), formData.get("position"));
            
            scrollTo({ top: 0, behavior: "smooth" });
        }, 100);

        await pushMatchScoutingData().catch((error) => {
            console.warn("Failed to upload match scouting data", error);
            toast.error("Failed to upload match scouting data to the server");
        });
    }

    /**
     * Export field data as JSON
     */
    function exportFields(): void {
        const json = JSON.stringify(fields, null, 2);
        const blob = new Blob([json], { type: "application/json" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = "fields.json";
        a.click();
        URL.revokeObjectURL(url);
    }

    /**
     * Get match scouting field presets from the server
     */
    async function getPresets(): Promise<void> {
        await getMatchScoutingFieldPresetsFieldsGetPresetsGet().then((response) => {
            if (response.status === 200) {
                presets = response.data
            }
        });
    }

    /**
     * Given a field, recursively create it and any of it's children on the server
     * 
     * Used when importing fields from a preset
     * 
     * @param field The field to create
     * @param parentUuid The UUID of the parent, if recurring
     */
    async function createFieldRecursive(field: SeasonMatchScoutingField, parentUuid: string | null = null): Promise<void> {
        const body = {
            uuid: field.uuid,
            name: field.name,
            description: field.description,
            season_uuid: season_uuid,
            field_type: field.field_type,
            stat_type: field.stat_type,
            required: field.required,
            order: field.order,
            organization_uuid: field.organization_id ?? null as string | null,
            parent_uuid: parentUuid ?? null as string | null,
            options: field.options ?? {
                choices: [],
                minimum: 0,
                maximum: 0,
                default: 0
            },
            game_piece_uuid: field.game_piece_uuid ?? null as string | null,
        }

        // Create field / section
        await createSeasonFieldFieldsSeasonSeasonUuidCreatePost(season_uuid, body).then(async (response) => {
            if (response.status === 200) {
                const newUuid = field?.uuid;
                if (!newUuid) return;
    
                // Recurse into children
                if (field.field_type === "section" && Array.isArray(field.fields)) {
                    for (const child of field.fields) {
                        await createFieldRecursive(child, newUuid);
                    }
                }
            }

        }).catch((error) => {
            console.error("Failed to create field:", field.name, error);
            toast.error(`Failed to create field '${field.name}'`);
            return;
        });
    }

    /**
     * Given a list of new fields that the admin is importing, upload them to the server and fetch the new ones
     * 
     * @param newFields The fields to import
     */
    async function importFieldsToServer(newFields: SeasonMatchScoutingField[]): void {
        await clearSeasonFieldsFieldsSeasonSeasonUuidClearDelete(season_uuid).then(async (response) => {
            if (response.status === 200) {
                for (const field of newFields) {
                    await createFieldRecursive(field, null);
                }
        
                // Refresh structure once everything is done
                toast.success("Fields imported", { duration: 5000 });
                getStructure();
            }
        });
    }

    /**
     * Import match scouting fields from JSON
     */
    function importAsFile(): void {
        if (!fieldFile) {toast.error("No file selected"); return;}
        const file: File = fieldFile[0];

        if (file && file.type === "application/json") {
            const reader = new FileReader();
            reader.onload = () => {
                try {
                    const newFields = JSON.parse(reader.result as string);

                    if (newFields.length > 0) {
                        importFieldsToServer(newFields);

                    } else {
                        toast.error("File is empty or invalid", { duration: 5000 });
                        fieldFile = null;
                    }
                } catch (error) {
                    console.error(error);
                    toast.error("Invalid file", { duration: 5000 });
                    fieldFile = null;
                }
            };
            reader.readAsText(file);
        } else {
            toast.error("Invalid file type", { duration: 5000 });
            fieldFile = null;
        }
    }

    /**
     * Import the selected field preset to the server
     */
    function importAsPreset(): void {
        if (selectedPreset) {
            importFieldsToServer(selectedPreset.preset);
        }
    }

    /**
     * Given a list of fields, normalize their order attribute to match their order in the list
     * 
     * @param fields The fields to normalize
     */
    function normalizeOrders(fields: SeasonMatchScoutingField[]): SeasonMatchScoutingField[] {
        return fields.map((item, index) => ({
            ...item,
            order: index,
            fields: Array.isArray(item.fields)
                ? normalizeOrders(item.fields)
                : item.fields
        }));
    }

    /**
     * Handler for when a drag item is being hovered over a drop area
     * 
     * @param e Drag and drop event
     */
    function handleDndConsider(e: Event) {
        fields = e.detail.items;
    }

    /**
     * Handler for when a drag item has been released into a drop area
     * 
     * @param e Drag and drop event
     */
    async function handleDndFinalize(e: Event) {
        fields = normalizeOrders(e.detail.items);
        await updateOrders();
    }

    /**
     * Recursively flatten fields into fields with no child fields
     * 
     * Used when updating field orders to the server
     * 
     * @param fields The fields to flatten
     * @param result If recursing, the results so far
     * @param parent_uuid If recursing, the parent UUID
     */
    function flattenFields(fields, result = [], parent_uuid = null) {
        for (const item of fields) {
            result.push({
                uuid: item.uuid,
                order: item.order,
                parent_uuid: parent_uuid
            });
            
            if (Array.isArray(item.fields)) {
                flattenFields(item.fields, result, item.uuid);
            }
        }
        return result;
    }

    /**
     * Update the order attribute for the current fields
     */
    async function updateOrders() {
        const flatFields = flattenFields(fields);

        await moveMatchScoutingFieldsFieldsSeasonUuidReorderPatch(season_uuid, flatFields).then((response) => {
            if (response.status !== 200) {
                toast.error("Failed to reorder fields", { duration: 5000 });
            }
        });
    }

    /**
     * Wait 100ms for season_uuid to populate before intializing the component
     */
    onMount(async () => {   
        while (!season_uuid) {
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        getStructure();
        getGamePieces();
    });

    /**
     * When season_uuid or year changes, initalize the component again
     */
    $effect(() => {
        season_uuid;
        year;

        getStructure();
        getGamePieces();
    })
</script>

<div class="flex flex-col gap-4">
    {#if season_uuid == ""}
        <Skeleton class="w-lg h-32" />
        <Skeleton class="w-lg h-32" />
        <Skeleton class="w-lg h-32" />
    {:else}
        {#if editable}
            <Card.Root class="w-auto min-w-64">
                <Card.Content>
                    <div class="flex flex-col gap-4">
                        <Button onclick={() => addFieldDialogOpen.set(true)}><PlusCircleIcon weight="bold" /> Add Field</Button>
                        <Button onclick={() => addSectionDialogOpen.set(true)}><FolderPlusIcon weight="bold" /> Add Section</Button>
                        <div class="flex flex-row gap-2 flex-wrap">
                            <Dialog.Root onOpenChange={getPresets}>
                                <Dialog.Trigger>
                                    <Button variant="outline" class="w-full"><DownloadSimpleIcon weight="bold" /> Import Fields</Button>
                                </Dialog.Trigger>
                                <Dialog.Content>
                                    <Dialog.Title>Import Field Data</Dialog.Title>
                                    <Dialog.Description>Import Field Data either from a JSON file, or from a preset provided by the server.</Dialog.Description>

                                    <Alert.Root>
                                        <InfoIcon weight="bold" />
                                        <Alert.Description>Game pieces are stored in the preset by UUID. If this preset came from a different server, the game piece won't be added correctly. Use repair mode to fix these fields.</Alert.Description>
                                    </Alert.Root>
                                    
                                    <Alert.Root variant="destructive">
                                        <WarningIcon weight="bold" />
                                        <Alert.Description>All existing field data for this season will be permanently overwritten</Alert.Description>
                                    </Alert.Root>

                                    <p class="font-bold">Upload a JSON file:</p>
                                    <div class="flex flex-row gap-2 flex-wrap">
                                        <Input type="file" accept="application/json" id="presetfile" bind:files={fieldFile} />
                                        <Button variant="outline" size="icon-sm" onclick={() => fieldFile = null}><TrashIcon weight="bold" /></Button>

                                        <Dialog.Close>
                                            <Button onclick={() => importAsFile()} disabled={fieldFile == null}>Import JSON File</Button>
                                        </Dialog.Close>
                                    </div>

                                    <p class="font-bold">Choose a preset:</p>
                                    <div class="flex flex-row gap-2 flex-wrap">
                                        {#if presets == null}
                                            <CircleNotchIcon class="animate-spin" size={22} />
                                            <p>Loading presets...</p>
                                        {:else if presets.length == 0}
                                            <p class="text-muted-foreground">No presets found on the server</p>
                                        {:else}
                                            <Select.Root
                                                type="single"
                                                name="preset"
                                                id="preset"
                                                bind:value={selectedPresetName}
                                                >
                                                <Select.Trigger>
                                                    {selectedPresetName ?? "Select a preset"}
                                                </Select.Trigger>

                                                <Select.Content>
                                                    {#each presets as preset}
                                                    <Select.Item
                                                        value={preset.name}
                                                        label={preset.name}
                                                    />
                                                    {/each}
                                                </Select.Content>
                                            </Select.Root>

                                            <Button variant="outline" size="icon-sm" onclick={() => selectedPresetName = null}><XCircleIcon weight="bold" /></Button>

                                            <Dialog.Close>
                                                <Button onclick={() => importAsPreset()} disabled={selectedPresetName == null}>Import Preset</Button>
                                            </Dialog.Close>
                                        {/if}
                                    </div>

                                    <Dialog.Footer>
                                        <Dialog.Close>
                                            <Button variant="outline">Close</Button>
                                        </Dialog.Close>
                                    </Dialog.Footer>
                                </Dialog.Content>
                            </Dialog.Root>
                            <Button variant="outline" onclick={() => exportFields()} class="flex-2"><ExportIcon weight="bold" /> Export Fields as JSON</Button>

                            <Dialog.Root>
                                <Dialog.Trigger>
                                    <Button variant="outline" size="icon"><InfoIcon weight="bold" /></Button>
                                </Dialog.Trigger>
                                <Dialog.Content>
                                    <Dialog.Title>About Exported Field Data</Dialog.Title>
                                    <Dialog.Description>Exported data can be imported on another server, or used to add a season preset into the repo at the start of a new season.</Dialog.Description>

                                    <Dialog.Footer>
                                        <Dialog.Close>
                                            <Button variant="outline">Close</Button>
                                        </Dialog.Close>
                                    </Dialog.Footer>
                                </Dialog.Content>
                            </Dialog.Root>
                        </div>
                    </div>
                </Card.Content>
            </Card.Root>
        {/if}
            
        {#if fields.length > 0}
            <Separator orientation="horizontal" />
            <form method="post" onsubmit={submit} class="flex flex-col gap-2 items-center" id="match-scouting-form">
                {#if editable}
                    <Card.Root class="w-auto min-w-64">
                        <Card.Content>
                            <div class="flex flex-row gap-2 items-center">
                                <InfoIcon weight="bold"/>
                                <p class="text-sm">Team number, match number, and match type fields are visible during full match scouting</p>
                            </div>
                        </Card.Content>
                    </Card.Root>
                {:else}
                    <MatchScoutingTeamInfo event_data={event_data} bind:this={matchScoutingTeamInfoChild} />
                {/if}

                {#snippet questionsBlock()}
                    {#each fields as field (field.uuid)}
                        <div animate:flip={{duration: 100}}>
                            {#if field.field_type === "string"}
                                <StringField field={field} editable={editable} getFields={getStructure}/>
                            {:else if field.field_type === "large_number"}
                                <LargeNumberField field={field} editable={editable} getFields={getStructure}/>
                            {:else if field.field_type === "small_number"}
                                <SmallNumberField field={field} editable={editable} getFields={getStructure}/>
                            {:else if field.field_type === "coarse_small_number"}
                                <CoarseSmallNumberField field={field} editable={editable} getFields={getStructure}/>
                            {:else if field.field_type === "boolean"}
                                <BooleanField field={field} editable={editable} getFields={getStructure}/>
                            {:else if field.field_type === "choice"}
                                <ChoiceField field={field} editable={editable} getFields={getStructure}/>
                            {:else if field.field_type === "multiple_choice"}
                                <MultipleChoiceField field={field} editable={editable} getFields={getStructure}/>
                            {:else if field.field_type === "section"}
                                <Section 
                                    bind:field={fields[fields.indexOf(field)]} 
                                    editable={editable} 
                                    getFields={getStructure}
                                    updateOrders={updateOrders}
                                />
                            {/if}
                        </div>
                    {/each}
                {/snippet}

                {#if editable}
                    <div class="flex flex-col gap-2 max-w-screen w-full md:w-lg" use:dragHandleZone={{items: fields, flipDurationMs: 100, dropTargetStyle: {outline: 'var(--primary) dashed 2px', borderRadius: 'var(--radius)'}}} onconsider={handleDndConsider} onfinalize={handleDndFinalize}>
                        {@render questionsBlock()}
                    </div>
                {:else}
                    <div class="flex flex-col gap-2 max-w-screen w-full md:w-lg">
                        {@render questionsBlock()}
                    </div>
                {/if}

                {#if !editable}
                    <MatchScoutingSubmit />
                {/if}
            </form>
        {:else}
            <p class="text-muted-foreground">No fields found</p>
        {/if}
    {/if}
</div>


<AddFieldDialog seasonUuid={season_uuid} gamePieces={gamePieces} getStructure={getStructure} />
<AddSectionDialog seasonUuid={season_uuid} getStructure={getStructure} />