import { type Fact } from "@fragments-sdk/core";

import { createSourceBackedComponentResolver } from "../source/component-resolver.js";
import type { UniversalComponentFact } from "../ucf/types.js";
import { aggregateComponentUsage } from "./aggregate.js";
import {
  callSiteKey,
  callSiteOwners,
  unresolvedImportsForFile,
} from "./component-adoption-source.js";
import { extractJsxFacts } from "./jsx.js";
import type {
  AnalyzeReactComponentAdoptionInput,
  ReactComponentAdoptionAnalysis,
  ReactComponentAdoptionCallSite,
  ReactComponentAdoptionComponent,
  ReactComponentAdoptionDefinition,
  ReactComponentAdoptionPropUsage,
  ReactComponentAdoptionRef,
  ReactComponentAdoptionTopImporter,
  ReactComponentAdoptionUnresolvedImport,
} from "./component-adoption-types.js";
import type { ComponentCallSite } from "./types.js";

export type {
  AnalyzeReactComponentAdoptionInput,
  ReactComponentAdoptionAnalysis,
  ReactComponentAdoptionCallSite,
  ReactComponentAdoptionComponent,
  ReactComponentAdoptionDefinition,
  ReactComponentAdoptionIdentity,
  ReactComponentAdoptionIdentityInput,
  ReactComponentAdoptionPropUsage,
  ReactComponentAdoptionRef,
  ReactComponentAdoptionTopImporter,
  ReactComponentAdoptionUnresolvedImport,
} from "./component-adoption-types.js";

interface ComponentGraph {
  usedBy: Map<string, Set<string>>;
  uses: Map<string, Set<string>>;
}

export async function analyzeReactComponentAdoption(
  args: AnalyzeReactComponentAdoptionInput,
): Promise<ReactComponentAdoptionAnalysis> {
  const definitions = componentDefinitions(args.ucfs, args.createIdentity);
  const resolver = await createSourceBackedComponentResolver({
    source: args.source,
    files: args.files,
    aliases: componentAliases(definitions),
  });
  const facts: Fact[] = [];
  const ownerByCallSite = new Map<string, string>();
  const unresolvedImports: ReactComponentAdoptionUnresolvedImport[] = [];
  const jsxFiles = args.files.filter((file) => isJsxSourceFile(file.relPath));
  let processed = 0;

  for (const file of jsxFiles) {
    const content = await args.source.readFile(file.relPath);
    if (content !== null) {
      const fileFacts = extractJsxFacts(content, file.relPath, {
        resolveComponentId: resolver,
      });
      facts.push(...fileFacts);
      for (const [key, owner] of callSiteOwners(content, file.relPath)) {
        ownerByCallSite.set(key, owner);
      }
      unresolvedImports.push(...unresolvedImportsForFile(fileFacts));
    }
    processed += 1;
    args.onProgress?.({
      filesProcessed: processed,
      totalFiles: jsxFiles.length,
    });
  }

  const callSites = aggregateComponentUsage(facts).callSites;

  return {
    definitions,
    components: componentsFromCallSites({
      definitions,
      callSites,
      ownerByCallSite,
    }),
    callSites,
    unresolvedImports,
  };
}

function componentsFromCallSites(args: {
  definitions: ReactComponentAdoptionDefinition[];
  callSites: ComponentCallSite[];
  ownerByCallSite: ReadonlyMap<string, string>;
}): ReactComponentAdoptionComponent[] {
  const callSitesByComponent = new Map<string, ComponentCallSite[]>();
  for (const site of args.callSites) {
    const key = String(site.componentId);
    const sites = callSitesByComponent.get(key) ?? [];
    sites.push(site);
    callSitesByComponent.set(key, sites);
  }

  const refsByKey = new Map(
    args.definitions.map((definition) => [
      String(definition.componentId),
      {
        componentName: definition.componentName,
        displayName: definition.componentName,
        filePath: definition.filePath,
        componentKey: definition.componentKey,
      },
    ]),
  );
  const graph = componentGraph(args);

  return args.definitions
    .map((definition) =>
      adoptionComponentFromDefinition({
        definition,
        callSites:
          callSitesByComponent.get(String(definition.componentId)) ?? [],
        usedBy: [...(graph.usedBy.get(String(definition.componentId)) ?? [])],
        uses: [...(graph.uses.get(String(definition.componentId)) ?? [])],
        refsByKey,
      }),
    )
    .sort(
      (a, b) =>
        a.componentName.localeCompare(b.componentName) ||
        a.filePath.localeCompare(b.filePath),
    );
}

function componentGraph(args: {
  definitions: ReactComponentAdoptionDefinition[];
  callSites: ComponentCallSite[];
  ownerByCallSite: ReadonlyMap<string, string>;
}): ComponentGraph {
  const usedBy = new Map<string, Set<string>>();
  const uses = new Map<string, Set<string>>();
  const definitionByFileAndLocalName = new Map<
    string,
    ReactComponentAdoptionDefinition
  >();

  for (const definition of args.definitions) {
    for (const localName of definition.localNames) {
      definitionByFileAndLocalName.set(
        definitionOwnerKey(definition.filePath, localName),
        definition,
      );
    }
  }

  for (const site of args.callSites) {
    const targetKey = String(site.componentId);
    const ownerName = args.ownerByCallSite.get(callSiteKey(site));
    if (!ownerName) continue;
    const source = definitionByFileAndLocalName.get(
      definitionOwnerKey(site.filePath, ownerName),
    );
    if (!source) continue;
    const sourceKey = String(source.componentId);
    if (sourceKey === targetKey) continue;

    const sourceUses = uses.get(sourceKey) ?? new Set<string>();
    sourceUses.add(targetKey);
    uses.set(sourceKey, sourceUses);

    const targetUsedBy = usedBy.get(targetKey) ?? new Set<string>();
    targetUsedBy.add(sourceKey);
    usedBy.set(targetKey, targetUsedBy);
  }

  return { usedBy, uses };
}

function componentDefinitions(
  ucfs: readonly UniversalComponentFact[],
  createIdentity: AnalyzeReactComponentAdoptionInput["createIdentity"],
): ReactComponentAdoptionDefinition[] {
  const out: ReactComponentAdoptionDefinition[] = [];
  const compoundAliases = compoundImplementationAliases(ucfs);

  for (const ucf of ucfs) {
    const alias = compoundAliases.get(ucf.componentName);
    if (alias) {
      const identity = createIdentity({
        componentName: alias.name,
        filePath: ucf.filePath,
      });
      out.push({
        componentName: alias.name,
        componentKey: identity.componentKey,
        componentId: identity.componentId,
        filePath: ucf.filePath,
        exportNames: exportNamesForUcf(ucf),
        publicExports: ucf.exportedFromBarrel ? 1 : 0,
        localNames: uniqueNames([ucf.componentName, alias.name]),
      });
      continue;
    }

    const identity = createIdentity({
      componentName: ucf.componentName,
      filePath: ucf.filePath,
    });
    out.push({
      componentName: ucf.componentName,
      componentKey: identity.componentKey,
      componentId: identity.componentId,
      filePath: ucf.filePath,
      exportNames: exportNamesForUcf(ucf),
      publicExports: ucf.exportedFromBarrel ? 1 : 0,
      localNames: uniqueNames([
        ucf.componentName,
        ...ucf.exports.flatMap((record) => [
          record.localName,
          record.exportedAs,
        ]),
      ]),
    });

    for (const child of ucf.compoundChildren) {
      const childName = `${ucf.componentName}.${child}`;
      const childIdentity = createIdentity({
        componentName: childName,
        filePath: ucf.filePath,
      });
      out.push({
        componentName: childName,
        componentKey: childIdentity.componentKey,
        componentId: childIdentity.componentId,
        filePath: ucf.filePath,
        exportNames: exportNamesForUcf(ucf).map((name) => `${name}.${child}`),
        publicExports: ucf.exportedFromBarrel ? 1 : 0,
        localNames: uniqueNames([childName, `${ucf.componentName}${child}`]),
      });
    }
  }

  return dedupeDefinitions(out);
}

function compoundImplementationAliases(
  ucfs: readonly UniversalComponentFact[],
): Map<string, { name: string; parentName: string }> {
  const aliases = new Map<string, { name: string; parentName: string }>();
  for (const ucf of ucfs) {
    for (const child of ucf.compoundChildren) {
      aliases.set(`${ucf.componentName}${child}`, {
        name: `${ucf.componentName}.${child}`,
        parentName: ucf.componentName,
      });
    }
  }
  return aliases;
}

function dedupeDefinitions(
  definitions: ReactComponentAdoptionDefinition[],
): ReactComponentAdoptionDefinition[] {
  const byKey = new Map<string, ReactComponentAdoptionDefinition>();
  for (const definition of definitions) {
    const existing = byKey.get(definition.componentKey);
    if (!existing) {
      byKey.set(definition.componentKey, definition);
      continue;
    }
    byKey.set(definition.componentKey, {
      ...existing,
      exportNames: uniqueNames([
        ...existing.exportNames,
        ...definition.exportNames,
      ]),
      publicExports: Math.max(existing.publicExports, definition.publicExports),
      localNames: uniqueNames([...existing.localNames, ...definition.localNames]),
    });
  }
  return [...byKey.values()];
}

function componentAliases(definitions: ReactComponentAdoptionDefinition[]) {
  return definitions.flatMap((definition) =>
    definition.exportNames.map((exportName) => ({
      componentId: definition.componentId,
      exportName,
      modulePath: definition.filePath,
    })),
  );
}

function exportNamesForUcf(ucf: UniversalComponentFact): string[] {
  const primaryExports = ucf.exports.filter((record) => record.isPrimary);
  const exportRecords =
    primaryExports.length > 0 ? primaryExports : ucf.exports;
  const names = new Set<string>(
    exportRecords.map((record) => record.exportedAs),
  );
  return [...names].filter(Boolean);
}

function adoptionComponentFromDefinition(args: {
  definition: ReactComponentAdoptionDefinition;
  callSites: ComponentCallSite[];
  usedBy: string[];
  uses: string[];
  refsByKey: ReadonlyMap<string, ReactComponentAdoptionRef>;
}): ReactComponentAdoptionComponent {
  const externalCallSites = args.callSites.filter(
    (site) => site.filePath !== args.definition.filePath,
  );
  const topImporters = topImportersFromCallSites(externalCallSites);
  const callSites = args.callSites.map(projectCallSite).slice(0, 20);
  const usedBy = refsFromKeys(args.usedBy, args.refsByKey);
  const uses = refsFromKeys(args.uses, args.refsByKey);

  return {
    componentName: args.definition.componentName,
    componentKey: args.definition.componentKey,
    componentId: args.definition.componentId,
    filePath: args.definition.filePath,
    totalImporters: topImporters.length,
    totalCallSites: args.callSites.length,
    topImporters,
    callSites,
    propUsage: summarizeCallSiteProps(callSites),
    usedBy,
    uses,
    publicExports: args.definition.publicExports,
    confidence: adoptionConfidence({
      topImporters: topImporters.length,
      usedBy: usedBy.length,
      publicExports: args.definition.publicExports,
    }),
    reviewReasons: reviewReasons({
      totalImporters: topImporters.length,
      usedBy: usedBy.length,
      publicExports: args.definition.publicExports,
    }),
    candidateForReview:
      topImporters.length === 0 &&
      usedBy.length === 0 &&
      args.definition.publicExports === 0,
  };
}

function refsFromKeys(
  keys: string[],
  refsByKey: ReadonlyMap<string, ReactComponentAdoptionRef>,
): ReactComponentAdoptionRef[] {
  return keys
    .map((key) => refsByKey.get(key))
    .filter((ref): ref is ReactComponentAdoptionRef => Boolean(ref))
    .sort(
      (a, b) =>
        a.componentName.localeCompare(b.componentName) ||
        a.filePath.localeCompare(b.filePath),
    );
}

function topImportersFromCallSites(
  callSites: ComponentCallSite[],
): ReactComponentAdoptionTopImporter[] {
  const byFile = new Map<string, ReactComponentAdoptionTopImporter>();
  for (const site of callSites) {
    if (byFile.has(site.filePath)) continue;
    byFile.set(site.filePath, {
      filePath: site.filePath,
      line: site.line,
      kind: "jsx",
      confidence: "confirmed",
    });
  }
  return [...byFile.values()].sort(
    (a, b) => a.filePath.localeCompare(b.filePath) || a.line - b.line,
  );
}

function projectCallSite(site: ComponentCallSite): ReactComponentAdoptionCallSite {
  return {
    filePath: site.filePath,
    line: site.line,
    column: site.column,
    props: site.props.map((prop) => ({
      name: prop.name,
      kind: prop.kind,
      ...(prop.value !== undefined && prop.value !== null
        ? { value: String(prop.value) }
        : {}),
    })),
  };
}

function adoptionConfidence(args: {
  topImporters: number;
  usedBy: number;
  publicExports: number;
}): ReactComponentAdoptionComponent["confidence"] {
  if (args.topImporters > 0 || args.usedBy > 0) return "confirmed";
  if (args.publicExports > 0) return "review";
  return "likely";
}

function summarizeCallSiteProps(
  callSites: ReactComponentAdoptionCallSite[],
): ReactComponentAdoptionPropUsage[] {
  const byName = new Map<
    string,
    {
      total: number;
      dynamic: number;
      spread: number;
      staticValues: Map<string, number>;
    }
  >();
  for (const site of callSites) {
    for (const prop of site.props) {
      const row = byName.get(prop.name) ?? {
        total: 0,
        dynamic: 0,
        spread: 0,
        staticValues: new Map<string, number>(),
      };
      row.total++;
      if (prop.kind === "dynamic") row.dynamic++;
      if (prop.kind === "spread") row.spread++;
      if (
        prop.kind !== "dynamic" &&
        prop.kind !== "spread" &&
        prop.value !== undefined
      ) {
        row.staticValues.set(
          prop.value,
          (row.staticValues.get(prop.value) ?? 0) + 1,
        );
      }
      byName.set(prop.name, row);
    }
  }

  return [...byName.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([name, row]) => ({
      name,
      total: row.total,
      dynamic: row.dynamic,
      spread: row.spread,
      staticValues: [...row.staticValues.entries()]
        .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
        .slice(0, 8)
        .map(([value, count]) => ({ value, count })),
    }));
}

function reviewReasons(args: {
  totalImporters: number;
  usedBy: number;
  publicExports: number;
}): string[] {
  return [
    "Projected from extract usage facts.",
    ...(args.totalImporters === 0
      ? ["No confirmed importing files found."]
      : []),
    ...(args.usedBy === 0 ? ["No component-level dependents found."] : []),
    ...(args.publicExports > 0
      ? ["Exported from a barrel/public entry file."]
      : []),
  ];
}

function definitionOwnerKey(filePath: string, localName: string): string {
  return `${filePath}:${localName}`;
}

function isJsxSourceFile(path: string): boolean {
  return /\.(?:tsx|jsx|js|ts)$/i.test(path) && !path.endsWith(".d.ts");
}

function uniqueNames(values: Array<string | undefined>): string[] {
  return [...new Set(values.filter((value): value is string => Boolean(value)))];
}
