import type { Story, World } from "../contexts/state";

// ─── Spec Types ───────────────────────────────────────────────────────────────

namespace CharacterCard {
    export interface CharaCardData {
        name: string;
        description: string;
        personality: string;
        scenario: string;
        first_mes: string;
        mes_example: string;
        system_prompt?: string;
        alternate_greetings?: string[];
        post_history_instructions?: string;
        creator_notes?: string;
        tags?: string[];
        creator?: string;
        character_version?: string;
    }

    export interface CharaCard {
        spec: "chara_card_v2";
        spec_version: string;
        data: CharaCardData;
    }

    // ─── Type Guard ───────────────────────────────────────────────────────────

    export function isCardData(obj: unknown): obj is CharaCard {
        if (typeof obj !== 'object' || obj === null) return false;
        const c = obj as Record<string, unknown>;
        return (
            typeof c.spec === 'string' &&
            ['chara_card_v2', 'chara_card_v3'].includes(c.spec) &&
            typeof c.spec_version === 'string' &&
            typeof c.data === 'object' && c.data !== null &&
            typeof (c.data as Record<string, unknown>).name === 'string'
        );
    }

    // ─── PNG Parsing ──────────────────────────────────────────────────────────

    /**
     * Extracts and parses the chara_card_v2 JSON embedded in a PNG tEXt chunk.
     * Returns null if the file is not a valid V2 character card.
     */
    export async function extractFromPng(file: File): Promise<CharaCard | null> {
        const buffer = await file.arrayBuffer();
        const bytes = new Uint8Array(buffer);

        // Validate PNG magic bytes
        const PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
        for (let i = 0; i < PNG_MAGIC.length; i++) {
            if (bytes[i] !== PNG_MAGIC[i]) return null;
        }

        const view = new DataView(buffer);
        let offset = 8; // skip magic

        while (offset + 12 <= bytes.length) {
            const length = view.getUint32(offset);
            const type = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);

            if (type === 'tEXt') {
                const dataStart = offset + 8;
                const dataEnd = dataStart + length;
                const chunkData = bytes.slice(dataStart, dataEnd);

                // Find null separator between keyword and text
                const nullIdx = chunkData.indexOf(0);
                if (nullIdx === -1) { offset += 12 + length; continue; }

                const keyword = new TextDecoder().decode(chunkData.slice(0, nullIdx));
                if (keyword !== 'chara') { offset += 12 + length; continue; }

                const encoded = new TextDecoder('latin1').decode(chunkData.slice(nullIdx + 1));
                const jsonBytes = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
                const json = new TextDecoder('utf-8').decode(jsonBytes);

                try {
                    const card = JSON.parse(json);
                    if (['chara_card_v2', 'chara_card_v3'].includes(card?.spec) && card?.data) {
                        return card as CharaCard;
                    }
                } catch {
                    return null;
                }
            }

            if (type === 'IEND') break;
            offset += 12 + length;
        }

        return null;
    }

    // ─── Formatting ───────────────────────────────────────────────────────────

    /**
     * Builds the systemInstructionOverride from a V2 card's data fields.
     * Mirrors the formatting style used in prompt.ts.
     */
    export function formatSystemPrompt(data: CharaCardData): string {
        const parts: string[] = [];

        parts.push(data.system_prompt ? data.system_prompt.trim() : '{{system}}');

        if (data.description?.trim()) {
            parts.push(`## {{char}}'s Description:\n${data.description.trim()}`);
        }

        if (data.personality?.trim()) {
            parts.push(`## {{char}}'s Personality:\n${data.personality.trim()}`);
        }

        if (data.scenario?.trim()) {
            parts.push(`## Scenario:\n${data.scenario.trim()}`);
        }

        if (data.mes_example?.trim()) {
            parts.push(`## {{char}}'s Example Response:\n${data.mes_example.trim()}`);
        }

        return parts.join('\n\n');
    }

    // ─── World Builder ────────────────────────────────────────────────────────

    function greetingTitle(mes: string): string {
        const snippet = mes.trim().replace(/[*"_-]/gi, '').split(/[,;:.?!]/i)[0];
        return snippet.length < mes.trim().length ? `${snippet}…` : snippet;
    }

    /**
     * Converts a V2 character card into a chat-only World.
     * Creates one story per greeting (first_mes + alternate_greetings).
     */
    export function toWorld(card: CharaCard): World {
        const { data } = card;

        const greetings: string[] = [
            ...(data.first_mes?.trim() ? [data.first_mes.trim()] : []),
            ...(data.alternate_greetings ?? []).filter(g => g?.trim()).map(g => g.trim()),
        ];

        const stories: Story[] = greetings.map(mes => ({
            id: crypto.randomUUID(),
            title: greetingTitle(mes),
            text: '',
            scratchpad: '',
            lore: [],
            characters: [],
            locations: [],
            chatMessages: [{ id: crypto.randomUUID(), role: 'assistant', content: mes }],
            chapters: [],
        }));

        if (stories.length === 0) {
            stories.push({
                id: crypto.randomUUID(),
                title: 'Chat',
                text: '',
                scratchpad: '',
                lore: [],
                characters: [],
                locations: [],
                chatMessages: [],
                chapters: [],
            });
        }

        return {
            id: crypto.randomUUID(),
            title: data.name || 'Imported Character',
            chatOnly: true,
            lore: [],
            characters: [],
            locations: [],
            stories,
            systemInstructionOverride: formatSystemPrompt(data),
        };
    }
}

export default CharacterCard;
