import ts from "typescript";

import { mapFileExports, mapImports, type FileExportRecord } from "../ucf/react-mappers.js";
import type {
  DefinitionKind,
  ExportRecord,
  Framework,
  UniversalComponentFact,
} from "../ucf/types.js";
import { ucfId } from "../ucf/ucf-id.js";
import type { ScanSource, ScanSourceFile } from "./types.js";

export interface QuickComponentCandidateProgress {
  filesProcessed: number;
  totalFiles: number;
  componentsFound: number;
}

export interface ExtractQuickComponentCandidatesInput {
  source: ScanSource;
  files: readonly ScanSourceFile[];
  sourceCommit: string;
  adapterVersion: string;
  onProgress?: (progress: QuickComponentCandidateProgress) => void;
}

export interface ExtractQuickComponentCandidatesResult {
  ucfs: UniversalComponentFact[];
  filesProcessed: number;
}

interface LocalCandidate {
  name: string;
  definitionKind: DefinitionKind;
  wrappedBy: string[];
}

interface CandidateExport {
  name: string;
  framework: Framework;
  definitionKind: DefinitionKind;
  wrappedBy: string[];
  exports: ExportRecord[];
  exportedFromBarrel: boolean;
  imports: UniversalComponentFact["imports"];
}

const REACT_WRAPPERS = new Set(["forwardRef", "memo"]);

export async function extractQuickComponentCandidatesFromSource(
  args: ExtractQuickComponentCandidatesInput
): Promise<ExtractQuickComponentCandidatesResult> {
  const capturedAt = new Date().toISOString();
  const ucfs: UniversalComponentFact[] = [];
  let filesProcessed = 0;

  args.onProgress?.({
    filesProcessed,
    totalFiles: args.files.length,
    componentsFound: ucfs.length,
  });

  for (const file of args.files) {
    const text = await args.source.readFile(file.relPath);
    if (text !== null) {
      ucfs.push(
        ...extractQuickCandidatesFromText({
          filePath: file.relPath,
          text,
          sourceCommit: args.sourceCommit,
          adapterVersion: args.adapterVersion,
          capturedAt,
        })
      );
    }
    filesProcessed++;
    args.onProgress?.({
      filesProcessed,
      totalFiles: args.files.length,
      componentsFound: ucfs.length,
    });
  }

  return {
    ucfs: dedupeUcfs(ucfs),
    filesProcessed,
  };
}

function extractQuickCandidatesFromText(args: {
  filePath: string;
  text: string;
  sourceCommit: string;
  adapterVersion: string;
  capturedAt: string;
}): UniversalComponentFact[] {
  if (isSfcPath(args.filePath)) {
    return extractSfcCandidate(args);
  }
  if (!isReactPath(args.filePath)) return [];

  const sourceFile = ts.createSourceFile(
    args.filePath,
    args.text,
    ts.ScriptTarget.Latest,
    true,
    scriptKindForPath(args.filePath)
  );
  const localCandidates = collectLocalCandidates(sourceFile);
  const fileExports = mapFileExports(sourceFile);
  const imports = mapImports(sourceFile);
  const candidateExports: CandidateExport[] = [];

  for (const record of fileExports.exportRecords) {
    const local = localCandidates.get(record.localName);
    if (!local) continue;
    candidateExports.push({
      name: componentNameForExport(record, local.name),
      framework: "react",
      definitionKind: local.definitionKind,
      wrappedBy: local.wrappedBy,
      exports: toExportRecords(fileExports.exportedFromBarrel, record),
      exportedFromBarrel: fileExports.exportedFromBarrel,
      imports,
    });
  }

  candidateExports.push(
    ...anonymousDefaultExports(sourceFile, args.filePath).map((candidate) => ({
      ...candidate,
      imports,
      exportedFromBarrel: fileExports.exportedFromBarrel,
    }))
  );

  return candidateExports.map((candidate) =>
    candidateToUcf({
      candidate,
      filePath: args.filePath,
      sourceCommit: args.sourceCommit,
      adapterVersion: args.adapterVersion,
      capturedAt: args.capturedAt,
    })
  );
}

function collectLocalCandidates(sourceFile: ts.SourceFile): Map<string, LocalCandidate> {
  const candidates = new Map<string, LocalCandidate>();

  for (const stmt of sourceFile.statements) {
    if (ts.isFunctionDeclaration(stmt) && stmt.name) {
      addFunctionCandidate(candidates, stmt.name.text, stmt);
    }
    if (ts.isVariableStatement(stmt)) {
      for (const decl of stmt.declarationList.declarations) {
        if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
        const shape = componentExpressionShape(decl.initializer);
        if (!shape) continue;
        addCandidate(candidates, {
          name: decl.name.text,
          definitionKind: shape.definitionKind,
          wrappedBy: shape.wrappedBy,
        });
      }
    }
  }

  return candidates;
}

function addFunctionCandidate(
  candidates: Map<string, LocalCandidate>,
  name: string,
  declaration: ts.FunctionDeclaration
) {
  if (!declaration.body || !containsRenderableJsx(declaration.body)) return;
  addCandidate(candidates, { name, definitionKind: "function", wrappedBy: [] });
}

function addCandidate(candidates: Map<string, LocalCandidate>, candidate: LocalCandidate) {
  if (!isLikelyComponentName(candidate.name)) return;
  candidates.set(candidate.name, candidate);
}

function componentExpressionShape(
  expression: ts.Expression
): Pick<LocalCandidate, "definitionKind" | "wrappedBy"> | null {
  if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
    return containsRenderableJsx(expression) ? { definitionKind: "function", wrappedBy: [] } : null;
  }
  if (ts.isCallExpression(expression)) {
    const wrapper = wrapperName(expression.expression);
    if (!wrapper) return null;
    const firstArg = expression.arguments[0];
    if (!firstArg || !ts.isExpression(firstArg)) return null;
    const inner = componentExpressionShape(firstArg);
    return inner ? { definitionKind: "wrapper", wrappedBy: [wrapper] } : null;
  }
  if (containsRenderableJsx(expression)) {
    return { definitionKind: "function", wrappedBy: [] };
  }
  return null;
}

function anonymousDefaultExports(sourceFile: ts.SourceFile, filePath: string): CandidateExport[] {
  const defaultName = componentNameFromPath(filePath);
  if (!defaultName) return [];
  const candidates: CandidateExport[] = [];

  for (const stmt of sourceFile.statements) {
    if (
      ts.isFunctionDeclaration(stmt) &&
      hasExportModifier(stmt) &&
      hasDefaultModifier(stmt) &&
      !stmt.name &&
      stmt.body &&
      containsRenderableJsx(stmt.body)
    ) {
      candidates.push(defaultCandidate(defaultName, "function", []));
    }
    if (ts.isExportAssignment(stmt) && !ts.isIdentifier(stmt.expression)) {
      const shape = componentExpressionShape(stmt.expression);
      if (shape) {
        candidates.push(defaultCandidate(defaultName, shape.definitionKind, shape.wrappedBy));
      }
    }
  }

  return candidates;
}

function defaultCandidate(
  name: string,
  definitionKind: DefinitionKind,
  wrappedBy: string[]
): CandidateExport {
  return {
    name,
    framework: "react",
    definitionKind,
    wrappedBy,
    exports: [
      {
        exportedAs: "default",
        localName: name,
        isDefault: true,
        fromBarrel: false,
        isPrimary: true,
      },
    ],
    exportedFromBarrel: false,
    imports: [],
  };
}

function extractSfcCandidate(args: {
  filePath: string;
  sourceCommit: string;
  adapterVersion: string;
  capturedAt: string;
}): UniversalComponentFact[] {
  const name = componentNameFromPath(args.filePath);
  if (!name) return [];
  const framework: Framework = args.filePath.endsWith(".svelte") ? "svelte" : "vue";
  return [
    candidateToUcf({
      candidate: {
        name,
        framework,
        definitionKind: "sfc",
        wrappedBy: [],
        exports: [
          {
            exportedAs: "default",
            localName: name,
            isDefault: true,
            fromBarrel: false,
            isPrimary: true,
          },
        ],
        exportedFromBarrel: false,
        imports: [],
      },
      filePath: args.filePath,
      sourceCommit: args.sourceCommit,
      adapterVersion: args.adapterVersion,
      capturedAt: args.capturedAt,
    }),
  ];
}

function candidateToUcf(args: {
  candidate: CandidateExport;
  filePath: string;
  sourceCommit: string;
  adapterVersion: string;
  capturedAt: string;
}): UniversalComponentFact {
  return {
    id: ucfId(args.filePath, args.candidate.name, args.candidate.framework, args.sourceCommit),
    filePath: args.filePath,
    componentName: args.candidate.name,
    framework: args.candidate.framework,
    sourceCommit: args.sourceCommit,
    capturedAt: args.capturedAt,
    adapterVersion: args.adapterVersion,
    definitionKind: args.candidate.definitionKind,
    wrappedBy: args.candidate.wrappedBy,
    imports: args.candidate.imports,
    exports: args.candidate.exports,
    exportedFromBarrel: args.candidate.exportedFromBarrel,
    rootElements: [],
    rootElementsTruncated: false,
    ariaRoles: [],
    ariaAttributes: {},
    props: [],
    events: [],
    slots: [],
    hasInternalState: false,
    hasEffects: false,
    isControlled: "unknown",
    compoundChildren: [],
    styleSystem: "unknown",
    classNamesUsed: [],
  };
}

function toExportRecords(exportedFromBarrel: boolean, record: FileExportRecord): ExportRecord[] {
  return [
    {
      exportedAs: record.exportedAs,
      localName: record.localName,
      isDefault: record.isDefault,
      fromBarrel: exportedFromBarrel,
      isPrimary: true,
    },
  ];
}

function componentNameForExport(record: FileExportRecord, localName: string): string {
  if (record.exportedAs !== "default" && isLikelyComponentName(record.exportedAs)) {
    return record.exportedAs;
  }
  return localName;
}

function containsRenderableJsx(node: ts.Node): boolean {
  let found = false;
  function visit(current: ts.Node) {
    if (found) return;
    if (
      ts.isJsxElement(current) ||
      ts.isJsxSelfClosingElement(current) ||
      ts.isJsxFragment(current)
    ) {
      found = true;
      return;
    }
    if (
      ts.isCallExpression(current) &&
      ts.isPropertyAccessExpression(current.expression) &&
      current.expression.expression.getText() === "React" &&
      current.expression.name.text === "createElement"
    ) {
      found = true;
      return;
    }
    ts.forEachChild(current, visit);
  }
  visit(node);
  return found;
}

function wrapperName(expression: ts.Expression): string | null {
  const name = ts.isIdentifier(expression)
    ? expression.text
    : ts.isPropertyAccessExpression(expression)
      ? expression.name.text
      : null;
  return name && REACT_WRAPPERS.has(name) ? name : null;
}

function componentNameFromPath(filePath: string): string | null {
  const withoutQuery = filePath.split("?")[0] ?? filePath;
  const parts = withoutQuery.split("/");
  const fileName = parts.at(-1) ?? "";
  const baseName = fileName.replace(/\.(?:tsx|jsx|ts|js|vue|svelte)$/i, "");
  const candidate = baseName.toLowerCase() === "index" ? parts.at(-2) : baseName;
  return candidate && isLikelyComponentName(candidate) ? candidate : null;
}

function isLikelyComponentName(name: string): boolean {
  return /^[A-Z][A-Za-z0-9_]*$/.test(name);
}

function isReactPath(path: string): boolean {
  return /\.(?:tsx|jsx|ts|js)$/i.test(path);
}

function isSfcPath(path: string): boolean {
  return /\.(?:vue|svelte)$/i.test(path);
}

function scriptKindForPath(path: string): ts.ScriptKind {
  if (path.endsWith(".jsx")) return ts.ScriptKind.JSX;
  if (path.endsWith(".js")) return ts.ScriptKind.JS;
  return ts.ScriptKind.TSX;
}

function hasExportModifier(node: ts.Node): boolean {
  const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
  return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
}

function hasDefaultModifier(node: ts.Node): boolean {
  const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
  return modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
}

function dedupeUcfs(ucfs: UniversalComponentFact[]): UniversalComponentFact[] {
  const byId = new Map<string, UniversalComponentFact>();
  for (const ucf of ucfs) {
    const existing = byId.get(ucf.id);
    if (!existing) {
      byId.set(ucf.id, ucf);
      continue;
    }
    byId.set(ucf.id, {
      ...existing,
      exports: mergeExports(existing.exports, ucf.exports),
    });
  }
  return [...byId.values()];
}

function mergeExports(
  left: ReadonlyArray<ExportRecord>,
  right: ReadonlyArray<ExportRecord>
): ExportRecord[] {
  const byKey = new Map<string, ExportRecord>();
  for (const record of [...left, ...right]) {
    byKey.set(`${record.exportedAs}:${record.localName ?? ""}`, record);
  }
  return [...byKey.values()];
}
