import type {
  Framework,
  PropFact,
  RootElementFact,
  SlotFact,
  StyleSystem,
  UniversalComponentFact,
} from "./types.js";
import { ucfId } from "./ucf-id.js";

const DEFAULT_ADAPTER_VERSION = "0.1.0";
const MAX_ROOT_ELEMENTS = 4;
const MAX_CLASSNAMES = 64;

export interface SfcInlineSource {
  filePath: string;
  source: string;
}

export interface ExtractUcfsForSfcOptions {
  sources: readonly SfcInlineSource[];
  sourceCommit: string;
  adapterVersion?: string;
}

export function extractUcfsForSfc(opts: ExtractUcfsForSfcOptions): UniversalComponentFact[] {
  const adapterVersion = opts.adapterVersion ?? DEFAULT_ADAPTER_VERSION;
  const capturedAt = new Date().toISOString();
  return opts.sources.flatMap((source) => {
    const framework = frameworkFromPath(source.filePath);
    if (!framework) return [];
    const componentName = componentNameFromPath(source.filePath);
    const templateBlock =
      framework === "vue"
        ? firstBlock(source.source, "template") ?? { content: "", offset: 0 }
        : { content: source.source, offset: 0 };
    const scriptBlocks =
      framework === "vue"
        ? blocks(source.source, "script")
        : [{ content: source.source, offset: 0 }];
    const script = scriptBlocks.map((block) => block.content).join("\n");
    const styleBlocks = [...source.source.matchAll(/<style\b([^>]*)>/gi)].map((match) => ({
      attrs: match[1] ?? "",
    }));
    const rootElements = rootElementsFromTemplate({
      template: templateBlock.content,
      fullSource: source.source,
      offset: templateBlock.offset,
      filePath: source.filePath,
    });

    return [
      {
        id: ucfId(source.filePath, componentName, framework, opts.sourceCommit),
        filePath: source.filePath,
        componentName,
        framework,
        sourceCommit: opts.sourceCommit,
        capturedAt,
        adapterVersion,
        definitionKind: "sfc",
        wrappedBy: [],
        imports: [],
        exports: [{ exportedAs: componentName, isPrimary: true }],
        exportedFromBarrel: false,
        rootElements,
        rootElementsTruncated: rootElements.length >= MAX_ROOT_ELEMENTS,
        ariaRoles: ariaRoles(templateBlock.content),
        ariaAttributes: ariaAttributes(templateBlock.content),
        props:
          framework === "vue"
            ? vueProps(scriptBlocks, source.source, source.filePath)
            : svelteProps(script, source.filePath),
        events: [],
        slots: slotsFromTemplate(templateBlock.content),
        hasInternalState: framework === "vue" ? hasVueState(script) : hasSvelteState(script),
        hasEffects: framework === "vue" ? hasVueEffects(script) : hasSvelteEffects(script),
        isControlled: "unknown",
        compoundChildren: [],
        styleSystem: styleSystemFromSfc(templateBlock.content, styleBlocks),
        classNamesUsed: classNamesFromTemplate(templateBlock.content),
      },
    ];
  });
}

function frameworkFromPath(path: string): Extract<Framework, "vue" | "svelte"> | null {
  if (path.endsWith(".vue")) return "vue";
  if (path.endsWith(".svelte")) return "svelte";
  return null;
}

function componentNameFromPath(path: string): string {
  const base = path.split("/").pop()?.replace(/\.(vue|svelte)$/i, "") || "Component";
  const normalized = base
    .split(/[^A-Za-z0-9]+/)
    .filter(Boolean)
    .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
    .join("");
  return /^[A-Z]/.test(normalized) ? normalized : `Component${normalized}`;
}

interface SfcBlock {
  content: string;
  offset: number;
}

function blocks(source: string, tag: string): SfcBlock[] {
  const pattern = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, "gi");
  return [...source.matchAll(pattern)].map((match) => ({
    content: match[1] ?? "",
    offset: (match.index ?? 0) + match[0].indexOf(match[1] ?? ""),
  }));
}

function firstBlock(source: string, tag: string): SfcBlock | null {
  const pattern = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i");
  const match = pattern.exec(source);
  if (!match) return null;
  const content = match[1] ?? "";
  return { content, offset: match.index + match[0].indexOf(content) };
}

function rootElementsFromTemplate(args: {
  template: string;
  fullSource: string;
  offset: number;
  filePath: string;
}): RootElementFact[] {
  const { template, fullSource, offset, filePath } = args;
  const cleaned = stripComments(template);
  const roots: RootElementFact[] = [];
  for (const match of cleaned.matchAll(/<([A-Za-z][\w:-]*)(\s[^>]*)?\/?>/g)) {
    const tag = match[1];
    if (!tag || tag === "template" || tag === "script" || tag === "style" || tag === "slot") {
      continue;
    }
    roots.push({
      tag,
      attrs: attrsFromString(match[2] ?? ""),
      loc: locationFromOffset(fullSource, offset + (match.index ?? 0), filePath),
    });
    if (roots.length >= MAX_ROOT_ELEMENTS) break;
  }
  return roots;
}

function attrsFromString(source: string): Record<string, string | true> {
  const attrs: Record<string, string | true> = {};
  for (const match of source.matchAll(/(?:^|\s)([:@A-Za-z_][\w:.-]*)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g)) {
    const name = match[1];
    if (!name) continue;
    attrs[name] = match[2] ?? match[3] ?? match[4] ?? true;
  }
  return attrs;
}

function vueProps(blocks: readonly SfcBlock[], fullSource: string, filePath: string): PropFact[] {
  for (const block of blocks) {
    const typeMatch = /defineProps\s*<\s*\{([\s\S]*?)\}\s*>/.exec(block.content);
    const typeLiteral = typeMatch?.[1];
    if (typeMatch && typeLiteral) {
      return propsFromTypeLiteral(
        typeLiteral,
        fullSource,
        filePath,
        block.offset + typeMatch.index + typeMatch[0].indexOf(typeLiteral)
      );
    }

    const objectMatch = /defineProps\s*\(\s*\{([\s\S]*?)\}\s*\)/.exec(block.content);
    const objectLiteral = objectMatch?.[1];
    if (!objectMatch || !objectLiteral) continue;
    const objectOffset = block.offset + objectMatch.index + objectMatch[0].indexOf(objectLiteral);
    return [...objectLiteral.matchAll(/([A-Za-z_$][\w$]*)\s*:/g)].map((match) => ({
      name: match[1]!,
      typeText: "unknown",
      loc: locationFromOffset(fullSource, objectOffset + (match.index ?? 0), filePath),
      optional: false,
      hasDefault: /default\s*:/.test(
        objectLiteral.slice(match.index ?? 0, (match.index ?? 0) + 120)
      ),
      isUnion: false,
    }));
  }
  return [];
}

function svelteProps(script: string, filePath: string): PropFact[] {
  return [...script.matchAll(/export\s+let\s+([A-Za-z_$][\w$]*)(?:\s*:\s*([^=;\n]+))?(?:\s*=\s*([^;\n]+))?/g)].map(
    (match) => propFromParts({
      name: match[1]!,
      typeText: match[2]?.trim() ?? "unknown",
      defaultValueText: match[3]?.trim(),
      optional: Boolean(match[3]),
      offset: match.index ?? 0,
      source: script,
      filePath,
    })
  );
}

function propsFromTypeLiteral(
  typeLiteral: string,
  source: string,
  filePath: string,
  offset: number
): PropFact[] {
  return [...typeLiteral.matchAll(/([A-Za-z_$][\w$]*)\??\s*:\s*([^;\n]+)[;\n]?/g)].map((match) =>
    propFromParts({
      name: match[1]!,
      typeText: match[2]?.trim() ?? "unknown",
      optional: typeLiteral
        .slice(match.index ?? 0, (match.index ?? 0) + match[0].length)
        .includes("?"),
      offset: offset + (match.index ?? 0),
      source,
      filePath,
    })
  );
}

function propFromParts(args: {
  name: string;
  typeText: string;
  optional: boolean;
  defaultValueText?: string;
  offset: number;
  source: string;
  filePath: string;
}): PropFact {
  const unionMembers = unionMembersFromType(args.typeText);
  return {
    name: args.name,
    typeText: args.typeText,
    loc: locationFromOffset(args.source, args.offset, args.filePath),
    optional: args.optional,
    hasDefault: args.defaultValueText !== undefined,
    ...(args.defaultValueText ? { defaultValueText: args.defaultValueText } : {}),
    isUnion: unionMembers.length > 0,
    ...(unionMembers.length ? { unionMembers } : {}),
  };
}

function unionMembersFromType(typeText: string): string[] {
  const parts = typeText.split("|").map((part) => part.trim());
  if (parts.length <= 1) return [];
  return parts;
}

function slotsFromTemplate(template: string): SlotFact[] {
  const slots = [...template.matchAll(/<slot\b([^>]*)>/gi)].map((match) => {
    const attrs = attrsFromString(match[1] ?? "");
    return {
      name: typeof attrs.name === "string" ? attrs.name : "default",
      scoped: Object.keys(attrs).some((key) => key.startsWith(":") || key.startsWith("v-bind")),
    };
  });
  return slots.length ? slots : [];
}

function styleSystemFromSfc(
  template: string,
  styleBlocks: ReadonlyArray<{ attrs: string }>
): StyleSystem {
  const classNames = classNamesFromTemplate(template);
  if (classNames.some((value) => /\b(?:bg|text|p|m|flex|grid|rounded)-/.test(value))) {
    return "tailwind";
  }
  if (styleBlocks.some((block) => /\blang=["']?s[ac]ss\b/i.test(block.attrs))) return "sass";
  if (styleBlocks.length > 0) return "plainCss";
  return "unknown";
}

function classNamesFromTemplate(template: string): string[] {
  const out = new Set<string>();
  for (const match of template.matchAll(/\bclass\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
    const value = match[1] ?? match[2] ?? "";
    for (const className of value.split(/\s+/).filter(Boolean)) {
      out.add(className);
      if (out.size >= MAX_CLASSNAMES) return [...out];
    }
  }
  return [...out];
}

function ariaRoles(template: string): string[] {
  return [...new Set([...template.matchAll(/\brole\s*=\s*(?:"([^"]*)"|'([^']*)')/g)].map((m) => m[1] ?? m[2] ?? ""))].filter(Boolean);
}

function ariaAttributes(template: string): Record<string, string | true> {
  const attrs: Record<string, string | true> = {};
  for (const match of template.matchAll(/\b(aria-[\w-]+)(?:=(?:"([^"]*)"|'([^']*)'))?/g)) {
    attrs[match[1]!] = match[2] ?? match[3] ?? true;
  }
  return attrs;
}

function hasVueState(script: string): boolean {
  return /\b(ref|reactive)\s*\(/.test(script);
}

function hasVueEffects(script: string): boolean {
  return /\b(watch|watchEffect|onMounted|onUpdated)\s*\(/.test(script);
}

function hasSvelteState(script: string): boolean {
  return /\blet\s+[A-Za-z_$][\w$]*\s*=/.test(script);
}

function hasSvelteEffects(script: string): boolean {
  return /\bonMount\s*\(|\$:\s*/.test(script);
}

function stripComments(source: string): string {
  return source.replace(/<!--[\s\S]*?-->/g, "");
}

function locationFromOffset(source: string, offset: number, file: string) {
  const prefix = source.slice(0, offset);
  const lines = prefix.split(/\r?\n/);
  return {
    file,
    line: lines.length,
    column: lines[lines.length - 1]?.length ?? 0,
  };
}
