import type { Plugin } from 'vite';
import { parse } from '@vue/compiler-sfc';
import { NodeTypes } from '@vue/compiler-core';
import type { ElementNode, TemplateChildNode, AttributeNode, DirectiveNode } from '@vue/compiler-core';
import MagicString from 'magic-string';
import { relative } from 'node:path';

import type { Manifest, ManifestEntry } from './types';
import {
    createEmpty,
    loadManifest,
    saveManifest,
    findBySignature,
    addEntry,
} from './manifest';
import {
    computeSignature,
} from './signature';
import type {
    SignatureNode,
    SignatureNodeProp,
    SignatureNodeChild,
} from './signature';

const INERT_TAGS = new Set([
    'template',
    'slot',
    'component',
    'Transition',
    'TransitionGroup',
    'Suspense',
    'KeepAlive',
]);

export interface DevIndicesPluginOptions {
    manifestPath: string;
    appRoot: string;
    enabled: boolean;
}

function nodeToSignatureNode(el: ElementNode): SignatureNode {
    const props: SignatureNodeProp[] = el.props.map((p): SignatureNodeProp => {
        if (p.type === NodeTypes.ATTRIBUTE) {
            const attr = p as AttributeNode;
            return { type: 'attribute', name: attr.name, value: attr.value?.content };
        }
        // DIRECTIVE node
        const dir = p as DirectiveNode;
        const argContent =
            dir.arg && 'content' in dir.arg ? (dir.arg as { content: string }).content : undefined;
        const expContent =
            dir.exp && 'content' in dir.exp ? (dir.exp as { content: string }).content : undefined;
        return {
            type: 'directive',
            name: dir.name,
            arg: argContent,
            exp: expContent,
        };
    });

    const children: SignatureNodeChild[] = el.children.map((c: TemplateChildNode): SignatureNodeChild => {
        if (c.type === NodeTypes.TEXT) return { type: 'text', content: c.content };
        if (c.type === NodeTypes.INTERPOLATION) return { type: 'interpolation' };
        if (c.type === NodeTypes.ELEMENT) return { type: 'element' };
        if (c.type === NodeTypes.COMMENT) return { type: 'comment' };
        return { type: 'element' };
    });

    return { tag: el.tag, props, children };
}

/**
 * Returns the offset in `source` where we should inject the attribute —
 * immediately before `>` (or before `/` in self-closing `/>`) of the opening tag.
 */
function elementOpenTagEndOffset(el: ElementNode, source: string): number {
    let i = el.loc.start.offset;
    let inQuote: string | null = null;
    while (i < source.length) {
        const ch = source[i];
        if (inQuote) {
            if (ch === inQuote) inQuote = null;
        } else if (ch === '"' || ch === "'") {
            inQuote = ch;
        } else if (ch === '>') {
            // self-closing `/>` — insert before the `/`
            if (source[i - 1] === '/') return i - 1;
            return i;
        }
        i++;
    }
    return -1;
}

export function devIndicesPlugin(options: DevIndicesPluginOptions): Plugin {
    const { manifestPath, appRoot, enabled } = options;
    let manifest: Manifest = createEmpty();
    let dirty = false;

    return {
        name: 'vite-plugin-dev-indices',

        buildStart() {
            if (!enabled) return;
            manifest = loadManifest(manifestPath);
        },

        transform(code: string, id: string) {
            if (!enabled) return null;
            if (!id.endsWith('.vue')) return null;
            // Skip query-suffixed Vue requests (e.g. ?type=style)
            if (id.includes('?')) return null;

            const { descriptor } = parse(code, { filename: id });
            if (!descriptor.template) return null;

            const ast = descriptor.template.ast;
            if (!ast) return null;

            const s = new MagicString(code);
            const relPath = relative(appRoot, id).replace(/\\/g, '/');

            const walk = (
                children: TemplateChildNode[],
                chain: string[],
            ): void => {
                const localOrdinals = new Map<string, number>();
                for (const child of children) {
                    if (child.type !== NodeTypes.ELEMENT) continue;
                    const el = child as ElementNode;
                    const tag = el.tag;

                    // Skip inert Vue compiler tags (no DOM presence) but still walk
                    // children with the tag preserved in ancestor chain so that
                    // descendant signatures remain stable.
                    if (INERT_TAGS.has(tag)) {
                        walk(el.children, [...chain, tag]);
                        continue;
                    }

                    const ord = localOrdinals.get(tag) ?? 0;
                    localOrdinals.set(tag, ord + 1);

                    const sigNode = nodeToSignatureNode(el);
                    const sig = computeSignature(sigNode, {
                        filePath: id,
                        appRoot,
                        ancestorChain: chain,
                        sameTagOrdinal: ord,
                    });

                    let assignedId = findBySignature(manifest, sig);
                    if (assignedId == null) {
                        const text =
                            el.children
                                .filter((c) => c.type === NodeTypes.TEXT)
                                .map((c) => (c as { content: string }).content)
                                .join(' ')
                                .trim()
                                .slice(0, 24) || null;

                        const entry: ManifestEntry = {
                            file: relPath,
                            line: el.loc.start.line,
                            tag,
                            parentChain: [...chain],
                            signature: sig,
                            text,
                            key: null,
                            ref: null,
                            createdAt: new Date().toISOString(),
                        };
                        assignedId = addEntry(manifest, entry);
                        dirty = true;
                    }

                    // Inject `data-dx="N"` into opening tag
                    const insertPos = elementOpenTagEndOffset(el, code);
                    if (insertPos >= 0) {
                        s.appendLeft(insertPos, ` data-dx="${assignedId}"`);
                    }

                    walk(el.children, [...chain, tag]);
                }
            };

            // Top-level ancestor = declaring component name (derived from file path)
            const declaringName = id.split(/[\\/]/).pop()!.replace(/\.vue$/, '');
            walk(ast.children, [declaringName]);

            // Flush manifest when dirty (after each transform to ensure safety)
            if (dirty) {
                saveManifest(manifestPath, manifest);
                dirty = false;
            }

            return {
                code: s.toString(),
                map: s.generateMap({ hires: true, source: id }),
            };
        },

        buildEnd() {
            if (!enabled) return;
            if (dirty) {
                saveManifest(manifestPath, manifest);
                dirty = false;
            }
        },
    };
}

export default devIndicesPlugin;
