import path from 'node:path';
import { ConfirmInput, Spinner } from '@inkjs/ui';
import { Box, Text } from 'ink';
import { useEffect, useRef, useState } from 'react';
import { DEFAULT_CONFIG_FILE_NAME, DEFAULT_OUTPUT_FILE_NAME } from '../constants.js';
import { generateTypes } from '../generateTypes.js';
import { getConfigs } from '../getConfigs.js';
import { MultiSelect } from './multiSelect.js';
import type { OpenAPITypescriptSource } from '../types.js';
import type { MultiSelectItem } from './multiSelect.js';

interface Config {
    dir: string;
    file: string;
    values: OpenAPITypescriptSource;
}

interface Result {
    config: Config;
    ok: boolean;
    error?: string;
}

type State =
    | { phase: 'searching' }
    | { phase: 'no-configs-confirm' }
    | { phase: 'no-configs' }
    | { phase: 'multi-select'; configs: Config[] }
    | {
          phase: 'generating';
          queue: Config[];
          skipped: Config[];
          doneIdx: number;
          results: Result[];
      }
    | { phase: 'done'; results: Result[]; skipped: Config[] };

function makeItems(configs: Config[]): MultiSelectItem[] {
    return configs.map((config, i) => ({
        id: `${config.dir}|${config.file}|${i}`,
        schemaUrl: config.values.schemaUrl,
        outputFile: config.values.outputFile,
    }));
}

function errorMessage(err: unknown): string {
    return err instanceof Error ? err.message : String(err);
}

function noConfigMessage(configPath?: string): string {
    return configPath
        ? `No config found at ${configPath}.`
        : `No ${DEFAULT_CONFIG_FILE_NAME} found in the current directory.`;
}

// Splits the configs into those the user selected (the queue) and the rest
// (skipped), preserving list order in both.
function partitionByIds(
    items: MultiSelectItem[],
    configs: Config[],
    selectedIds: string[]
): { queue: Config[]; skipped: Config[] } {
    const idSet = new Set(selectedIds);
    const queue: Config[] = [];
    const skipped: Config[] = [];

    items.forEach((item, i) => {
        const config = configs[i];
        if (!config) return;

        if (idSet.has(item.id)) {
            queue.push(config);
        } else {
            skipped.push(config);
        }
    });

    return { queue, skipped };
}

async function generateOne(config: Config): Promise<Result> {
    const { schemaUrl, outputFile, options } = config.values;
    const outputPath = path.resolve(config.dir, outputFile ?? DEFAULT_OUTPUT_FILE_NAME);

    try {
        await generateTypes(schemaUrl, outputPath, options);
        return { config, ok: true };
    } catch (err) {
        return { config, ok: false, error: errorMessage(err) };
    }
}

interface QueueItemProps {
    config: Config;
    result: Result | undefined;
    isActive: boolean;
}

function QueueItem(props: QueueItemProps) {
    const label = props.config.values.schemaUrl;

    if (props.result) {
        return (
            <Text color={props.result.ok ? 'green' : 'red'}>
                {props.result.ok ? '✔' : '✖'} {label}
                {props.result.error ? ` — ${props.result.error}` : ''}
            </Text>
        );
    }

    if (props.isActive) {
        return <Spinner label={label} />;
    }

    return <Text color="gray">◌ {label}</Text>;
}

interface SkippedItemProps {
    config: Config;
}

function SkippedItem(props: SkippedItemProps) {
    return <Text color="gray">⊘ {props.config.values.schemaUrl} (skipped)</Text>;
}

export interface GenerateCommandProps {
    configPath?: string;
    // When true (the `--yes` flag): skip the multi-select prompt, generate
    // every config, and on no configs just report it instead of offering init.
    autoConfirm?: boolean;
    onRequestInit: () => void;
    onComplete: (failedCount: number) => void;
}

export function GenerateCommand(props: GenerateCommandProps) {
    const [state, setState] = useState<State>({ phase: 'searching' });
    // Flipped on unmount so async work in flight stops touching state.
    const cancelledRef = useRef(false);

    // Generate each selected config in order, revealing progress as it goes,
    // then land on the terminal 'done' state. Driven by callbacks (the
    // multi-select submit, or the auto path below) — not by re-running effects.
    const runGeneration = async (queue: Config[], skipped: Config[]) => {
        const results: Result[] = [];
        setState({ phase: 'generating', queue, skipped, doneIdx: 0, results: [] });

        for (const config of queue) {
            const result = await generateOne(config);
            if (cancelledRef.current) return;

            results.push(result);
            setState({
                phase: 'generating',
                queue,
                skipped,
                doneIdx: results.length,
                results: [...results],
            });
        }

        setState({ phase: 'done', results, skipped });
    };

    // Load configs once on mount, then route: report when none are found,
    // generate straight away for a single config or --yes, otherwise prompt.
    useEffect(() => {
        cancelledRef.current = false;

        void (async () => {
            try {
                const configs = await getConfigs(props.configPath);
                if (cancelledRef.current) return;

                if (configs.length === 0) {
                    setState({
                        phase: props.autoConfirm ? 'no-configs' : 'no-configs-confirm',
                    });
                } else if (configs.length === 1 || props.autoConfirm) {
                    await runGeneration(configs, []);
                } else {
                    setState({ phase: 'multi-select', configs });
                }
            } catch (err) {
                if (cancelledRef.current) return;

                setState({
                    phase: 'done',
                    skipped: [],
                    results: [
                        {
                            config: {
                                dir: '',
                                file: props.configPath ?? '',
                                values: { schemaUrl: '' },
                            },
                            ok: false,
                            error: errorMessage(err),
                        },
                    ],
                });
            }
        })();

        return () => {
            cancelledRef.current = true;
        };
        // Mount-only: configPath/autoConfirm are fixed for this component's life.
    }, []);

    // Notify the parent once a terminal state has rendered,
    // so the done/summary frame is committed before the parent calls exit().
    useEffect(() => {
        if (state.phase === 'done') {
            props.onComplete(state.results.filter((result) => !result.ok).length);
        }
        if (state.phase === 'no-configs') {
            props.onComplete(0);
        }
    }, [state, props.onComplete]);

    if (state.phase === 'searching') {
        return <Spinner label="Searching for config files..." />;
    }

    if (state.phase === 'no-configs') {
        return <Text color="yellow">{noConfigMessage(props.configPath)}</Text>;
    }

    if (state.phase === 'no-configs-confirm') {
        return (
            <Box flexDirection="column">
                <Text color="yellow">{noConfigMessage(props.configPath)}</Text>
                <Box>
                    <Text>Create one now? (Y/n) </Text>
                    <ConfirmInput
                        onConfirm={props.onRequestInit}
                        onCancel={() => props.onComplete(0)}
                    />
                </Box>
            </Box>
        );
    }

    if (state.phase === 'multi-select') {
        const items = makeItems(state.configs);

        const handleSubmit = (selectedIds: string[]) => {
            const { queue, skipped } = partitionByIds(items, state.configs, selectedIds);

            void runGeneration(queue, skipped);
        };

        return (
            <Box flexDirection="column">
                <Text>Select configs to generate:</Text>
                <MultiSelect items={items} onSubmit={handleSubmit} />
            </Box>
        );
    }

    if (state.phase === 'generating') {
        return (
            <Box flexDirection="column">
                {state.queue.map((config, i) => (
                    <QueueItem
                        key={`${config.dir}|${config.file}|${i}`}
                        config={config}
                        result={state.results[i]}
                        isActive={i === state.doneIdx}
                    />
                ))}
                {state.skipped.map((config, i) => (
                    <SkippedItem key={`skip|${config.dir}|${config.file}|${i}`} config={config} />
                ))}
            </Box>
        );
    }

    const total = state.results.length;
    const failed = state.results.filter((result) => !result.ok).length;
    const succeeded = total - failed;
    const skippedCount = state.skipped.length;

    const summaryColor = failed === 0 ? 'green' : 'yellow';
    const summaryIcon = failed === 0 ? '✔' : '!';

    const summaryParts = [`${succeeded} generated`];
    if (failed > 0) summaryParts.push(`${failed} failed`);
    if (skippedCount > 0) summaryParts.push(`${skippedCount} skipped`);
    const summaryText = `Done. ${summaryParts.join(' · ')}.`;

    return (
        <Box flexDirection="column">
            {state.results.map((result, i) => (
                <Text
                    key={`${result.config.dir}|${result.config.file}|${i}`}
                    color={result.ok ? 'green' : 'red'}
                >
                    {result.ok ? '✔' : '✖'} {result.config.values.schemaUrl}
                    {result.error ? ` — ${result.error}` : ''}
                </Text>
            ))}
            {state.skipped.map((config, i) => (
                <SkippedItem key={`skip|${config.dir}|${config.file}|${i}`} config={config} />
            ))}
            <Text color={summaryColor}>
                {summaryIcon} {summaryText}
            </Text>
        </Box>
    );
}
