<!--
    ELEMENT: AE Obj Field Editor — NEW VERSION
    ========================================================================
    Full plan / rationale: documentation/PROJECT__AE_Obj_Field_Editor_New.md
    Original (still the live, working version — do not touch it yet):
    src/lib/elements/element_ae_obj_field_editor.svelte

    This file starts from the original's logic. The optimistic-update state
    machine (is_editing / draft_value / has_optimistic / display_value) is
    already hardened in production (see commit c3ec0f88e) — copy it forward
    as-is. Do not redesign it unless you find an actual bug; it is NOT part
    of this rewrite's scope.
-->
<script lang="ts" generics="T">
import { untrack } from 'svelte';
import type { Snippet } from 'svelte';
import {
    Check,
    CircleAlert,
    Eraser,
    LoaderCircle,
    Save,
    SquarePen,
    X
} from '@lucide/svelte';
import type { key_val } from '$lib/stores/ae_stores';
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api';
import { ae_util } from '$lib/ae_utils/ae_utils';
import AE_Comp_Editor_TipTap from '$lib/elements/element_editor_tiptap.svelte';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';

interface Props {
    // Core Identifiers
    id?: string;
    object_type: string;
    object_id: string;
    field_name: string;

    // Value Handling
    current_value: T;
    field_type?:
        | 'text'
        | 'textarea'
        | 'select'
        | 'tiptap'
        | 'codemirror'
        | 'checkbox'
        | 'date'
        | 'datetime'
        | 'number'
        | 'email'
        | 'url'
        | 'tel';
    allow_null?: boolean;

    // Select Options
    select_options?: key_val; // { value: label }

    // UI Configuration
    edit_label?: string;
    display_block?: boolean;
    display_absolute_edit?: boolean;
    display_modal?: boolean;
    modal_blocking?: boolean;
    modal_placement?: 'center' | 'above' | 'below' | 'left' | 'right';
    placeholder?: string;
    class_li?: string;
    textarea_rows?: number;

    // Behavior
    log_lvl?: number;

    // Callbacks
    on_success?: (data: any) => void;
    on_error?: (error: any) => void;
    on_open?: () => void | Promise<void>;

    // Snippets
    children?: Snippet;

    // State
    is_editing?: boolean;
}

let {
    id,
    object_type,
    object_id,
    field_name,
    current_value = $bindable(),
    field_type = 'text',
    allow_null = false,
    select_options = {},
    edit_label = 'Edit Field',
    display_block = false,
    display_absolute_edit = false,
    display_modal = true,
    modal_blocking = false,
    modal_placement = 'center',
    placeholder = 'Enter value...',
    class_li = '',
    textarea_rows = 4,
    log_lvl = 0,
    on_success,
    on_error,
    on_open,
    children,
    is_editing = $bindable(false)
}: Props = $props();

/**
 * to_input_value: stored value -> what the native <input> wants.
 */
function to_input_value(value: T, type: typeof field_type): any {
    if (value === null || value === undefined) return '';

    if (type === 'datetime') {
        return ae_util.iso_datetime_formatter(value as any, 'datetime_iso_no_seconds').replace(' ', 'T');
    }

    if (type === 'date') {
        return ae_util.iso_datetime_formatter(value as any, 'date_iso');
    }

    return value;
}

/**
 * from_input_value: native <input> value -> format backend expects.
 */
function from_input_value(value: any, type: typeof field_type): any {
    if (value === null || value === undefined || value === '') {
        return allow_null ? null : value;
    }

    if (type === 'datetime') {
        return value.replace('T', ' ') + ':00';
    }

    if (type === 'date') {
        return value;
    }

    if (type === 'number') {
        return Number(value);
    }

    return value;
}

/**
 * coerce_select_value: Native <select> values are always strings.
 * Coerce back to the real type based on the shape of current_value.
 */
function coerce_select_value(raw: string, reference: T): any {
    if (raw === 'null' || raw === '') return null;
    if (typeof reference === 'number') return Number(raw);
    if (typeof reference === 'boolean') return raw === 'true';
    return raw;
}

// Internal State
let patch_status = $state<'idle' | 'processing' | 'success' | 'error'>('idle');
let error_message = $state('');
let draft_value = $state<any>(null);
let input_ref = $state<HTMLElement | null>(null);
let dialog_ref = $state<HTMLDialogElement | null>(null);
let trigger_ref = $state<HTMLElement | null>(null);
let dialog_style = $state('');

// Optimistic display state machine
let has_optimistic = $state(false);
let display_value = $derived(has_optimistic ? draft_value : current_value);

// Sync draft with current_value when not editing
$effect(() => {
    if (!is_editing) {
        untrack(() => {
            if (!has_optimistic) {
                draft_value = to_input_value(current_value, field_type);
            }
        });
    }
});

// Clear optimistic once current_value catches up
$effect(() => {
    const formatted_current = to_input_value(current_value, field_type);
    if (has_optimistic && formatted_current === draft_value) {
        has_optimistic = false;
    }
});

// Autofocus when entering edit mode (for simple inputs)
$effect(() => {
    if (is_editing && input_ref) {
        untrack(() => input_ref?.focus());
    }
});

// Open/close the <dialog> in modal mode
$effect(() => {
    if (!display_modal || !dialog_ref) return;
    if (is_editing) {
        untrack(() => {
            if (!dialog_ref!.open) {
                if (modal_blocking) dialog_ref!.showModal();
                else dialog_ref!.show();
            }
        });
    } else {
        untrack(() => { if (dialog_ref!.open) dialog_ref!.close(); });
    }
});

// Non-modal: close when clicking outside the dialog (no backdrop to click)
$effect(() => {
    if (!display_modal || modal_blocking || !is_editing) return;
    function on_outside(e: PointerEvent) {
        if (dialog_ref && !dialog_ref.contains(e.target as Node)) cancel_edit();
    }
    document.addEventListener('pointerdown', on_outside);
    return () => document.removeEventListener('pointerdown', on_outside);
});

async function handle_patch() {
    if (log_lvl) console.log(`AE Field Editor (new): Patching ${object_type}.${field_name}...`);

    patch_status = 'processing';
    error_message = '';

    try {
        const final_value = from_input_value(draft_value, field_type);

        const result = await api.update_ae_obj({
            api_cfg: $ae_api,
            obj_type: object_type,
            obj_id: object_id,
            fields: { [field_name]: final_value },
            log_lvl
        });

        if (result) {
            patch_status = 'success';
            has_optimistic = true;
            if (on_success) on_success(result);

            setTimeout(() => {
                if (patch_status === 'success') {
                    patch_status = 'idle';
                    is_editing = false;
                }
            }, 800);
        } else {
            throw new Error('No data returned from update.');
        }
    } catch (error: any) {
        console.error('AE Field Editor (new): Patch failed.', error);
        patch_status = 'error';
        error_message = error?.message || 'Update failed.';
        if (on_error) on_error(error);
    }
}

function cancel_edit() {
    const value_changed = draft_value !== to_input_value(current_value, field_type);
    if (value_changed && patch_status !== 'success' && !confirm('Discard unsaved changes?')) return;
    has_optimistic = false;
    draft_value = to_input_value(current_value, field_type);
    is_editing = false;
    patch_status = 'idle';
    error_message = '';
}

function calc_dialog_pos() {
    if (!trigger_ref) return;
    const rect = trigger_ref.getBoundingClientRect();
    const gap = 8;
    const cx = rect.left + rect.width / 2;
    const cy = rect.top + rect.height / 2;

    // Use a fixed max-width for calculations (matching the CSS max-w-sm = 384px)
    // and an estimated height (200px) to clamp the anchor point safely.
    const dw = 384; 
    const dh = 240; 
    const padding = 16;

    let top: number, left: number, tx: string, ty: string;
    switch (modal_placement) {
        case 'above':
            top = rect.top - gap;   left = cx;              tx = '-50%'; ty = '-100%'; break;
        case 'below':
            top = rect.bottom + gap; left = cx;             tx = '-50%'; ty = '0';     break;
        case 'left':
            top = cy;               left = rect.left - gap; tx = '-100%'; ty = '-50%'; break;
        case 'right':
            top = cy;               left = rect.right + gap; tx = '0';   ty = '-50%'; break;
        default: // center over button
            top = cy;               left = cx;              tx = '-50%'; ty = '-50%';  break;
    }

    // Viewport clamping: adjust the anchor point if the dialog would overflow
    // This assumes the translate(-50%, -50%) etc. is applied.
    const vh = window.innerHeight;
    const vw = window.innerWidth;

    // Clamp Left/Right (X-axis)
    if (tx === '-50%') {
        left = Math.max(dw/2 + padding, Math.min(vw - dw/2 - padding, left));
    } else if (tx === '-100%') {
        left = Math.max(dw + padding, left);
    } else if (tx === '0') {
        left = Math.min(vw - dw - padding, left);
    }

    // Clamp Top/Bottom (Y-axis)
    if (ty === '-50%') {
        top = Math.max(dh/2 + padding, Math.min(vh - dh/2 - padding, top));
    } else if (ty === '-100%') {
        top = Math.max(dh + padding, top);
    } else if (ty === '0') {
        top = Math.min(vh - dh - padding, top);
    }

    dialog_style = `margin:0;top:${top}px;left:${left}px;transform:translate(${tx},${ty});`;
}

function toggle_edit() {
    if (is_editing) cancel_edit();
    else {
        has_optimistic = false;
        draft_value = to_input_value(current_value, field_type);
        if (display_modal) calc_dialog_pos();
        is_editing = true;
        if (on_open) on_open();
    }
}

function handle_keydown(e: KeyboardEvent) {
    if (e.key === 'Escape') cancel_edit();
}
</script>

{#snippet edit_panel()}
    <header class="mb-2 flex items-center justify-between">
        <span class="text-xs font-bold tracking-wider uppercase opacity-60">
            {edit_label || field_name}
        </span>
        <div class="flex gap-1">
            <button
                type="button"
                class="btn-icon btn-icon-sm preset-tonal-surface"
                onclick={cancel_edit}
                aria-label="Cancel editing"
                title="Cancel editing"
                disabled={patch_status === 'processing'}>
                <X size="14" />
            </button>
        </div>
    </header>

    <div class="input_container mb-3">
        {#if field_type === 'textarea'}
            <textarea
                {id}
                bind:this={input_ref}
                bind:value={draft_value}
                rows={textarea_rows}
                class="textarea"
                {placeholder}></textarea>
        {:else if field_type === 'select'}
            <div class="relative">
                <select
                    {id}
                    bind:this={input_ref}
                    value={draft_value}
                    onchange={(e) => draft_value = coerce_select_value(e.currentTarget.value, current_value)}
                    class="select pr-10">
                    {#if allow_null}
                        <option value={null}>-- None --</option>
                    {/if}
                    {#if Object.keys(select_options).length === 0}
                        <option value="" disabled>Loading options...</option>
                    {/if}
                    {#each Object.entries(select_options) as [val, label] (val)}
                        <option value={val}>{label}</option>
                    {/each}
                </select>
                {#if Object.keys(select_options).length === 0}
                    <div class="absolute inset-y-0 right-8 flex items-center">
                        <LoaderCircle size="14" class="animate-spin opacity-50" />
                    </div>
                {/if}
            </div>
        {:else if field_type === 'checkbox'}
            <label class="flex items-center space-x-2">
                <input
                    {id}
                    type="checkbox"
                    bind:this={input_ref}
                    bind:checked={draft_value}
                    class="checkbox" />
                <span>{draft_value ? 'True' : 'False'}</span>
            </label>
        {:else if field_type === 'tiptap'}
            <AE_Comp_Editor_TipTap
                bind:content={draft_value}
                {placeholder} />
        {:else if field_type === 'codemirror'}
            <AE_Comp_Editor_CodeMirror
                bind:content={draft_value}
                {placeholder} />
        {:else if field_type === 'date'}
            <input
                {id}
                type="date"
                bind:this={input_ref}
                bind:value={draft_value}
                class="input" />
        {:else if field_type === 'datetime'}
            <input
                {id}
                type="datetime-local"
                bind:this={input_ref}
                bind:value={draft_value}
                class="input" />
        {:else}
            <input
                {id}
                type={field_type === 'number' ? 'number' : field_type === 'email' ? 'email' : field_type === 'url' ? 'url' : field_type === 'tel' ? 'tel' : 'text'}
                bind:this={input_ref}
                bind:value={draft_value}
                class="input"
                {placeholder}
                onkeydown={(e) => e.key === 'Enter' && handle_patch()} />
        {/if}
    </div>

    <footer class="flex items-center justify-between">
        <div class="status_indicator text-xs">
            {#if patch_status === 'processing'}
                <span class="text-primary-500 flex items-center gap-1">
                    <LoaderCircle size="12" class="animate-spin" /> Saving...
                </span>
            {:else if patch_status === 'success'}
                <span class="text-success-500 flex items-center gap-1">
                    <Check size="12" /> Saved
                </span>
            {:else if patch_status === 'error'}
                <div class="text-error-500 flex flex-col gap-0.5">
                    <span class="flex items-center gap-1 font-bold">
                        <CircleAlert size="12" /> Error
                    </span>
                    <span class="opacity-80">{error_message}</span>
                </div>
            {/if}
        </div>

        <div class="actions flex gap-2">
            {#if allow_null && draft_value !== null}
                <button
                    type="button"
                    class="btn btn-sm preset-tonal-error"
                    onclick={() => (draft_value = null)}
                    aria-label="Clear value"
                    title="Clear value">
                    <Eraser size="14" class="mr-1" />
                    Clear
                </button>
            {/if}
            <button
                type="button"
                class="btn btn-sm preset-filled-primary-500"
                onclick={handle_patch}
                aria-label="Save changes"
                title="Save changes"
                disabled={patch_status === 'processing' || draft_value === to_input_value(current_value, field_type)}>
                {#if patch_status === 'processing'}
                    <LoaderCircle size="14" class="mr-1 animate-spin" />
                {:else}
                    <Save size="14" class="mr-1" />
                {/if}
                Save
            </button>
        </div>
    </footer>
{/snippet}

<div
    class="ae_field_editor group relative {class_li}"
    class:block={display_block}
    class:inline-block={!display_block}
    role="none"
    onkeydown={handle_keydown}>

    <!-- VIEW MODE: stays visible in modal mode so layout doesn't shift -->
    <div class="view_wrapper flex items-center gap-2" class:hidden={is_editing && !display_modal}>
        <div class="content_render grow">
            {#if children}
                {@render children()}
            {:else if field_type === 'checkbox'}
                <span class="badge {display_value ? 'preset-tonal-success' : 'preset-tonal-surface'}">
                    {display_value ? 'True' : 'False'}
                </span>
            {:else if field_type === 'tiptap' || field_type === 'codemirror'}
                <div class="prose dark:prose-invert max-w-none">
                    <!-- eslint-disable-next-line svelte/no-at-html-tags -->
                    {@html display_value || '<span class="opacity-50 italic">Empty</span>'}
                </div>
            {:else}
                <span class:opacity-50={!display_value}>
                    {display_value || 'Not set'}
                </span>
            {/if}
        </div>

        <button
            bind:this={trigger_ref}
            type="button"
            class="btn-icon btn-icon-sm preset-tonal-warning opacity-20 transition-opacity hover:opacity-100"
            class:invisible={!$ae_loc.edit_mode}
            class:pointer-events-none={!$ae_loc.edit_mode}
            tabindex={$ae_loc.edit_mode ? 0 : -1}
            onclick={toggle_edit}
            aria-label="Edit {edit_label || field_name}"
            title="Edit {edit_label || field_name}">
            <SquarePen size="14" />
        </button>
    </div>

    <!-- EDIT MODE — inline or absolute (not used in modal mode) -->
    {#if is_editing && !display_modal}
        <div
            class="edit_wrapper border-warning-500/50 bg-surface-50-950 z-50 rounded-lg border-2 border-dashed p-3 shadow-xl"
            class:absolute={display_absolute_edit}
            class:top-0={display_absolute_edit}
            class:left-0={display_absolute_edit}
            class:w-full={display_absolute_edit}>
            {@render edit_panel()}
        </div>
    {/if}
</div>

<!-- MODAL MODE — <dialog> renders in the browser top layer, never clipped by table/overflow -->
{#if display_modal}
    <dialog
        bind:this={dialog_ref}
        style={dialog_style}
        class="ae_field_editor_dialog border-surface-200-800 w-full max-w-sm rounded-xl border p-4 shadow-2xl"
        oncancel={(e) => { e.preventDefault(); cancel_edit(); }}
        onclick={(e) => { if (e.target === dialog_ref) cancel_edit(); }}
        onkeydown={(e) => { if (e.key === 'Escape') cancel_edit(); }}>
        {@render edit_panel()}
    </dialog>
{/if}

<style>
.ae_field_editor :global(.btn-icon-sm) {
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
}

/* Solid card-face background using theme variables directly —
   Skeleton tonal classes are semi-transparent by design, unusable on dialogs.
   position:fixed + z-index required for non-modal show() which doesn't get
   the browser top-layer; redundant but harmless for showModal(). */
:global(.ae_field_editor_dialog) {
    position: fixed;
    z-index: 9999;
    background-color: var(--color-surface-50);
    color: var(--color-surface-950);
}
:global(.dark .ae_field_editor_dialog) {
    background-color: var(--color-surface-900);
    color: var(--color-surface-50);
}
:global(.ae_field_editor_dialog::backdrop) {
    background: rgba(0, 0, 0, 0.35);
    backdrop-filter: blur(2px);
}
</style>
