import { Button, Label, Modal } from '@/Components/ui';
import {
    type DevicePresetDto,
    type FactorScreenshotGaps,
    type FormFactor,
} from '@/types/layout';
import { useUpgradeModal } from '@/upgrade/useUpgradeModal';
import { useEffect, useMemo, useRef, useState } from 'react';
import { PlanGateError, runExport, type ExportProgress } from './export/exportFlow';
import { factorLabel } from './factorLabels';
import { useEditor } from './store';

type ScreenRef = {
    id: number;
    name: string;
};

type Scope = 'current' | 'all';

type Props = {
    open: boolean;
    onClose: () => void;
    projectId: number;
    projectName: string;
    screens: ScreenRef[];
    activeScreenId: number;
    devicePresets?: DevicePresetDto[];
    formFactor: FormFactor;
    /** Full set of factors the project ships. Drives the `{factor}/` ZIP
     *  segment for multi-factor projects. */
    activeFormFactors?: FormFactor[];
    /** Per-factor screenshot completeness (server-computed). Selected
     *  factors with missing frame screenshots surface a pre-flight warning
     *  so the user doesn't ship empty device bezels unknowingly. */
    screenshotGaps?: FactorScreenshotGaps;
};

type Phase =
    | { kind: 'idle' }
    | { kind: 'running'; progress: ExportProgress }
    | { kind: 'done'; downloadUrl: string }
    | { kind: 'error'; message: string }
    | { kind: 'plan-gate'; message: string; pricingUrl: string | null };

export default function ExportModal({
    open,
    onClose,
    projectId,
    projectName,
    screens,
    activeScreenId,
    devicePresets = [],
    formFactor,
    activeFormFactors,
    screenshotGaps,
}: Props) {
    const openUpgrade = useUpgradeModal((s) => s.openModal);
    const activeLocales = useEditor((s) => s.activeLocales);
    const activeLocale = useEditor((s) => s.activeLocale);
    const defaultLocale = useEditor((s) => s.defaultLocale);
    const multiLocale = activeLocales.length > 1;

    // Full factor set the project ships (drives the picker rows below).
    const availableFactors: FormFactor[] = useMemo(
        () => activeFormFactors ?? [formFactor],
        [activeFormFactors, formFactor],
    );

    const [scope, setScope] = useState<Scope>('all');
    // Factor selection — defaults to ALL active factors checked so the
    // common case (export everything) stays one click. Users can uncheck
    // individual factors to produce subset ZIPs (e.g. iPhone-only review
    // build) without mutating the project's active_form_factors. Initial
    // state is recomputed every time the modal opens via the open-effect
    // below so a project-settings change between sessions reflects.
    const [selectedFactors, setSelectedFactors] = useState<FormFactor[]>(availableFactors);
    // Locale selection — same model as factors. Single-locale projects
    // skip the picker entirely; multi-locale projects start with every
    // active locale checked and let the user opt out per-locale.
    const [selectedLocales, setSelectedLocales] = useState<string[]>(activeLocales);
    const [phase, setPhase] = useState<Phase>({ kind: 'idle' });
    const abortRef = useRef<AbortController | null>(null);

    useEffect(() => {
        if (!open) {
            abortRef.current?.abort();
            abortRef.current = null;
            setPhase({ kind: 'idle' });
            setScope('all');
            setSelectedFactors(availableFactors);
            setSelectedLocales(activeLocales);
        } else {
            // Re-seed on open so the picker reflects the current project's
            // active set (the modal can stay mounted across factor toggles
            // / locale changes in Project Settings).
            setSelectedFactors(availableFactors);
            setSelectedLocales(activeLocales);
        }
    }, [open, availableFactors, activeLocales]);

    const toggleFactor = (factor: FormFactor) => {
        setSelectedFactors((current) => {
            if (current.includes(factor)) {
                const next = current.filter((f) => f !== factor);
                // Always keep at least one factor selected — the start
                // button also disables on zero, but the no-op here keeps
                // the UI feeling deliberate instead of letting a click
                // empty the set silently.
                return next.length > 0 ? next : current;
            }
            // Maintain availableFactors order in selected list so the ZIP
            // segment order is stable across re-selections.
            return availableFactors.filter(
                (f) => current.includes(f) || f === factor,
            );
        });
    };

    const toggleLocale = (locale: string) => {
        setSelectedLocales((current) => {
            if (current.includes(locale)) {
                const next = current.filter((l) => l !== locale);
                // Same min-one constraint as factors — the ZIP would have
                // nothing to render without at least one locale.
                return next.length > 0 ? next : current;
            }
            // Preserve activeLocales order so `{locale}/` segment order
            // stays consistent across re-selections.
            return activeLocales.filter(
                (l) => current.includes(l) || l === locale,
            );
        });
    };

    const screenCount = scope === 'current' ? 1 : screens.length;
    const localeCount = multiLocale ? selectedLocales.length : 1;
    const factorCount = selectedFactors.length;
    const tileCount = screenCount * localeCount * factorCount;

    // Pre-flight completeness: selected factors whose frames are missing
    // screenshots. Non-blocking — kit authors may genuinely want empty
    // bezels — but loud, because the alternative is discovering empty
    // devices after opening the ZIP.
    const gapFactors = selectedFactors.filter(
        (f) => (screenshotGaps?.[f]?.missing ?? 0) > 0,
    );
    const running = phase.kind === 'running';

    const canStart =
        !running
        && screens.length > 0
        && selectedFactors.length > 0
        && (!multiLocale || selectedLocales.length > 0);

    const start = async () => {
        const screenIds =
            scope === 'current' ? [activeScreenId] : screens.map((s) => s.id);

        // Locale set: single-locale projects always export at the project
        // default (no `{locale}/` ZIP segment); multi-locale projects use
        // whatever the user checked in the Locales picker. Server fans out
        // + ZIP gets the `{locale}/` segment when >1 locale ships.
        const locales: string[] = multiLocale ? selectedLocales : [defaultLocale];

        const controller = new AbortController();
        abortRef.current = controller;
        setPhase({ kind: 'running', progress: { phase: 'creating' } });

        try {
            await runExport({
                projectId,
                projectName,
                screenIds,
                devicePresets,
                // No devicePresetIds — server uses each factor's canonical
                // canvas. Apple + Play Store auto-scale within a factor's
                // tier set, so the per-preset picker stays out of the modal.
                locales,
                factors: selectedFactors,
                onProgress: (progress) =>
                    setPhase({ kind: 'running', progress }),
                signal: controller.signal,
            });
            setPhase({ kind: 'done', downloadUrl: '' });
        } catch (err) {
            if (err instanceof DOMException && err.name === 'AbortError') {
                setPhase({ kind: 'idle' });
                return;
            }
            if (err instanceof PlanGateError) {
                setPhase({
                    kind: 'plan-gate',
                    message: err.message,
                    pricingUrl: err.pricingUrl,
                });
                return;
            }
            setPhase({
                kind: 'error',
                message: err instanceof Error ? err.message : 'Export failed.',
            });
        } finally {
            abortRef.current = null;
        }
    };

    return (
        <Modal
            open={open}
            onClose={onClose}
            closeOnBackdrop={false}
            closeOnEsc={!running}
            size="md"
            data-testid="export-modal"
        >
            <Modal.Header
                title={projectName}
                subtitle={
                    <span data-testid="export-tile-count">
                        Render {tileCount}{' '}
                        {tileCount === 1 ? 'screenshot' : 'screenshots'} as PNG
                        · downloads as ZIP.
                    </span>
                }
            />
            <Modal.Body
                className={running ? 'pointer-events-none opacity-60' : undefined}
                aria-disabled={running}
            >
                    <div className="space-y-2">
                        <Label>Scope</Label>
                        <div className="flex flex-col gap-1">
                            <ScopeOption
                                testId="export-scope-all"
                                checked={scope === 'all'}
                                onChange={() => setScope('all')}
                                label={`All screens (${screens.length})`}
                                hint="One PNG per screen."
                            />
                            <ScopeOption
                                testId="export-scope-current"
                                checked={scope === 'current'}
                                onChange={() => setScope('current')}
                                label="Current screen only"
                                hint="Just the screen you're editing."
                            />
                        </div>
                    </div>

                    {multiLocale && (
                        <div className="mt-3 space-y-2" data-testid="export-locales-row">
                            <Label>Locales</Label>
                            <div className="flex flex-col gap-1">
                                {activeLocales.map((locale) => {
                                    const checked = selectedLocales.includes(locale);
                                    const isOnly = checked && selectedLocales.length === 1;
                                    const isActive = locale === activeLocale;
                                    const isDefault = locale === defaultLocale;
                                    return (
                                        <label
                                            key={locale}
                                            className={`flex cursor-pointer items-start gap-3 rounded-md border px-3 py-2 text-sm transition-colors ${
                                                checked
                                                    ? 'border-accent bg-accent/5'
                                                    : 'border-border hover:bg-muted/40'
                                            } ${isOnly ? 'cursor-not-allowed opacity-90' : ''}`}
                                            data-testid={`export-locale-${locale}`}
                                        >
                                            <input
                                                type="checkbox"
                                                className="mt-1 h-3.5 w-3.5 accent-accent"
                                                checked={checked}
                                                disabled={isOnly}
                                                onChange={() => toggleLocale(locale)}
                                                data-testid={`export-locale-${locale}-input`}
                                            />
                                            <span className="flex flex-col">
                                                <span className="text-foreground">
                                                    {locale}
                                                    {(isActive || isDefault) && (
                                                        <span className="ml-2 text-[10px] uppercase tracking-wider text-muted-foreground">
                                                            {isActive && isDefault
                                                                ? 'active · default'
                                                                : isActive
                                                                  ? 'active'
                                                                  : 'default'}
                                                        </span>
                                                    )}
                                                </span>
                                            </span>
                                        </label>
                                    );
                                })}
                            </div>
                            <p className="text-[11px] text-muted-foreground">
                                Each checked locale gets a {'`{locale}/`'} segment in the ZIP. At least one stays selected.
                            </p>
                        </div>
                    )}

                    {availableFactors.length > 1 && (
                        <div className="mt-3 space-y-2" data-testid="export-factors-row">
                            <Label>Devices</Label>
                            <div className="flex flex-col gap-1">
                                {availableFactors.map((factor) => {
                                    const checked = selectedFactors.includes(factor);
                                    const isOnly = checked && selectedFactors.length === 1;
                                    return (
                                        <label
                                            key={factor}
                                            className={`flex cursor-pointer items-start gap-3 rounded-md border px-3 py-2 text-sm transition-colors ${
                                                checked
                                                    ? 'border-accent bg-accent/5'
                                                    : 'border-border hover:bg-muted/40'
                                            } ${isOnly ? 'cursor-not-allowed opacity-90' : ''}`}
                                            data-testid={`export-factor-${factor}`}
                                        >
                                            <input
                                                type="checkbox"
                                                className="mt-1 h-3.5 w-3.5 accent-accent"
                                                checked={checked}
                                                disabled={isOnly}
                                                onChange={() => toggleFactor(factor)}
                                                data-testid={`export-factor-${factor}-input`}
                                            />
                                            <span className="flex flex-col">
                                                <span className="text-foreground">
                                                    {factorLabel(factor)}
                                                </span>
                                                {(screenshotGaps?.[factor]?.missing ?? 0) > 0 && (
                                                    <span className="text-[11px] text-foreground">
                                                        {screenshotGaps![factor]!.missing}{' '}
                                                        {screenshotGaps![factor]!.missing === 1
                                                            ? 'frame is missing a screenshot'
                                                            : 'frames are missing screenshots'}
                                                    </span>
                                                )}
                                            </span>
                                        </label>
                                    );
                                })}
                            </div>
                            <p className="text-[11px] text-muted-foreground">
                                Each checked device gets a {'`{factor}/`'} segment in the ZIP. At least one stays selected.
                            </p>
                        </div>
                    )}

                    {gapFactors.length > 0 && (
                        <div
                            role="alert"
                            data-testid="export-screenshot-gaps"
                            className="mt-3 rounded-md border border-warning/30 bg-warning/10 p-3 text-[12px] text-foreground"
                        >
                            <p className="font-medium">
                                Some devices are missing screenshots
                            </p>
                            <ul className="mt-1 list-inside list-disc">
                                {gapFactors.map((f) => (
                                    <li key={f}>
                                        {factorLabel(f)} — {screenshotGaps![f]!.missing} of{' '}
                                        {screenshotGaps![f]!.frames}{' '}
                                        {screenshotGaps![f]!.frames === 1 ? 'frame' : 'frames'}
                                    </li>
                                ))}
                            </ul>
                            <p className="mt-1">
                                These export as empty device frames. Switch to each
                                device in the editor to add its screenshots.
                            </p>
                        </div>
                    )}

            </Modal.Body>
            <Modal.Footer className="flex-col items-stretch gap-3">
                {phase.kind !== 'idle' && (
                    <div data-testid="export-status">
                        {phase.kind === 'running' && (
                            <ProgressView progress={phase.progress} total={tileCount} />
                        )}
                        {phase.kind === 'error' && (
                            <p
                                data-testid="export-error"
                                className="text-[11px] font-medium text-destructive"
                            >
                                {phase.message}
                            </p>
                        )}
                        {phase.kind === 'plan-gate' && (
                            <div
                                data-testid="export-plan-gate"
                                className="rounded-md border border-warning/30 bg-warning/10 p-3 text-sm text-foreground"
                            >
                                <p className="font-medium">Export is a paid feature</p>
                                <p className="mt-1 text-[12px]">{phase.message}</p>
                            </div>
                        )}
                        {phase.kind === 'done' && (
                            <p
                                className="text-[11px] font-medium text-accent"
                                data-testid="export-ready"
                            >
                                Saved to your downloads folder.
                            </p>
                        )}
                    </div>
                )}

                <div className="flex items-center justify-end gap-2">
                    {phase.kind === 'done' ? (
                        <Button
                            type="button"
                            variant="primary"
                            onClick={onClose}
                            data-testid="export-close"
                        >
                            Done
                        </Button>
                    ) : phase.kind === 'plan-gate' ? (
                        <>
                            <Button
                                type="button"
                                variant="ghost"
                                onClick={onClose}
                                data-testid="export-close"
                            >
                                Close
                            </Button>
                            <Button
                                type="button"
                                variant="primary"
                                onClick={() => {
                                    onClose();
                                    openUpgrade('export');
                                }}
                                data-testid="export-upgrade"
                            >
                                Upgrade plan
                            </Button>
                        </>
                    ) : (
                        <>
                            <Button
                                type="button"
                                variant="ghost"
                                onClick={onClose}
                                disabled={running}
                                data-testid="export-cancel"
                            >
                                Cancel
                            </Button>
                            <Button
                                type="button"
                                onClick={() => void start()}
                                disabled={!canStart}
                                data-testid="export-start"
                            >
                                {running ? 'Exporting…' : 'Start export'}
                            </Button>
                        </>
                    )}
                </div>
            </Modal.Footer>
        </Modal>
    );
}

type ScopeOptionProps = {
    testId: string;
    checked: boolean;
    onChange: () => void;
    label: string;
    hint: string;
};

function ScopeOption({ testId, checked, onChange, label, hint }: ScopeOptionProps) {
    return (
        <label
            className={`flex cursor-pointer items-start gap-3 rounded-md border px-3 py-2 text-sm transition-colors ${
                checked
                    ? 'border-accent bg-accent/5'
                    : 'border-border hover:bg-muted/40'
            }`}
        >
            <input
                type="radio"
                name="export-scope"
                className="mt-1 h-3.5 w-3.5 accent-accent"
                checked={checked}
                onChange={onChange}
                data-testid={testId}
            />
            <span className="flex flex-col">
                <span className="text-foreground">{label}</span>
                <span className="text-xs text-muted-foreground">{hint}</span>
            </span>
        </label>
    );
}

function ProgressView({
    progress,
    total,
}: {
    progress: ExportProgress;
    total: number;
}) {
    const label = describe(progress, total);
    const ratio = ratioFor(progress, total);

    return (
        <div className="flex flex-col gap-1.5">
            <div
                className="text-[11px] font-medium text-muted-foreground"
                data-testid="export-phase"
                data-phase={progress.phase}
            >
                {label}
            </div>
            <div className="h-1 w-full overflow-hidden rounded-full bg-muted">
                <div
                    className="h-full bg-accent transition-[width] duration-200"
                    style={{ width: `${Math.round(ratio * 100)}%` }}
                />
            </div>
        </div>
    );
}

function describe(progress: ExportProgress, total: number): string {
    switch (progress.phase) {
        case 'creating':
            return 'Preparing export…';
        case 'rendering':
            return `Rendering ${progress.current + 1} / ${total}`;
        case 'zipping':
            return 'Building ZIP…';
        case 'done':
            return 'Done.';
    }
}

function ratioFor(progress: ExportProgress, total: number): number {
    if (total === 0) return 0;
    switch (progress.phase) {
        case 'creating':
            return 0.02;
        case 'rendering':
            return (progress.current + 0.5) / (total + 1);
        case 'zipping':
            return total / (total + 1);
        case 'done':
            return 1;
    }
}
