import { parse, type ParserOptions } from "@babel/parser";
import _traverse, { type NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import type {
  ConformChange,
  ConformPropMapping,
  ConformSuggestion,
  Finding,
} from "@fragments-sdk/core";

const traverse = (_traverse as unknown as { default?: typeof _traverse }).default ?? _traverse;

const PARSER_OPTIONS: ParserOptions = {
  sourceType: "module",
  plugins: [
    "jsx",
    "typescript",
    ["decorators", { decoratorsBeforeExport: true }],
    "classProperties",
    "classPrivateProperties",
    "classPrivateMethods",
    "exportDefaultFrom",
    "exportNamespaceFrom",
    "dynamicImport",
    "nullishCoalescingOperator",
    "optionalChaining",
    "objectRestSpread",
  ],
};

interface ComponentConformInput {
  source: string;
  findings: readonly Finding[];
}

interface ComponentConformResult {
  source: string;
  changes: ConformChange[];
  suggestions: ConformSuggestion[];
  importsAdded: string[];
}

interface Edit {
  start: number;
  end: number;
  replacement: string;
}

interface ElementCandidate {
  key: string;
  line: number;
  column: number;
  path: NodePath<t.JSXElement>;
}

export function applyComponentConformEdits(input: ComponentConformInput): ComponentConformResult {
  const componentFindings = input.findings.filter(isComponentFinding);
  if (componentFindings.length === 0) {
    return { source: input.source, changes: [], suggestions: [], importsAdded: [] };
  }

  const propResult = applyPropRenames(input.source, componentFindings);
  const importResult = ensureNamedImports(propResult.source, componentFindings);
  return {
    source: importResult.source,
    changes: [...propResult.changes, ...importResult.changes],
    suggestions: propResult.suggestions,
    importsAdded: importResult.importsAdded,
  };
}

function applyPropRenames(
  source: string,
  findings: readonly Finding[]
): { source: string; changes: ConformChange[]; suggestions: ConformSuggestion[] } {
  const ast = parseSafely(source);
  if (!ast) return { source, changes: [], suggestions: [] };

  const elementsByLocation = new Map<string, ElementCandidate>();
  const candidatesByComponentLine = new Map<string, ElementCandidate[]>();
  traverse(ast, {
    JSXElement(path) {
      const loc = path.node.openingElement.loc?.start;
      if (!loc) return;
      const key = `${loc.line}:${loc.column}`;
      const candidate = { key, line: loc.line, column: loc.column, path };
      elementsByLocation.set(key, candidate);
      const name = jsxIdentifierName(path.node.openingElement.name);
      if (!name) return;
      const listKey = componentLineKey(name, loc.line);
      const list = candidatesByComponentLine.get(listKey) ?? [];
      list.push(candidate);
      candidatesByComponentLine.set(listKey, list);
    },
  });

  const edits: Edit[] = [];
  const changes: ConformChange[] = [];
  const suggestions: ConformSuggestion[] = [];
  const usedElements = new Set<string>();
  for (const finding of findings) {
    const component = stringAttr(finding.attributes, "suggestedComponent");
    if (!component) continue;
    const mappings = propMappings(finding.attributes);
    if (mappings.length === 0) continue;
    const candidate = findElementForPropRename({
      finding,
      component,
      mappings,
      elementsByLocation,
      candidatesByComponentLine,
      usedElements,
    });
    if (!candidate) continue;
    usedElements.add(candidate.key);
    const { path } = candidate;

    for (const mapping of mappings) {
      applyPropMapping({ mapping, path, finding, component, edits, changes, suggestions });
    }
  }

  return { source: applyEdits(source, edits), changes, suggestions };
}

/**
 * Apply one prop mapping to a resolved element. Three shapes:
 *  - No value table: rename the attribute name only (cross-library prop rename).
 *  - Value table + the literal value is a confirmed key: rewrite name and/or
 *    value together.
 *  - Value table + a non-literal or unmapped value: withhold the rewrite and
 *    surface a `map-prop-value` suggestion. We never invent a translation.
 */
function applyPropMapping(args: {
  mapping: ConformPropMapping;
  path: NodePath<t.JSXElement>;
  finding: Finding;
  component: string;
  edits: Edit[];
  changes: ConformChange[];
  suggestions: ConformSuggestion[];
}): void {
  const { mapping, path, finding, component, edits, changes, suggestions } = args;
  if (!isIdentifier(mapping.rawProp)) return;
  const renames = mapping.rawProp !== mapping.canonicalProp;
  if (renames && !isIdentifier(mapping.canonicalProp)) return;

  const valueMap = usableValueMap(mapping);
  if (!valueMap && !renames) return;

  const attr = findJsxAttribute(path, mapping.rawProp);
  if (!attr || typeof attr.name.start !== "number" || typeof attr.name.end !== "number") return;

  if (!valueMap) {
    edits.push({ start: attr.name.start, end: attr.name.end, replacement: mapping.canonicalProp });
    changes.push({
      kind: "rename-prop",
      ruleId: finding.ruleId,
      code: finding.code,
      severity: levelToSeverity(finding.level),
      message: `Renamed ${mapping.rawProp} to ${mapping.canonicalProp} for <${component}>.`,
      before: mapping.rawProp,
      after: mapping.canonicalProp,
      canonical: component,
      confidence: "high",
      location: toLocation(finding.location),
    });
    return;
  }

  const literal = stringLiteralValue(attr);
  const mapped = literal ? valueMap[literal.value] : undefined;
  if (literal === null || mapped === undefined) {
    suggestions.push(mapValueSuggestion({ mapping, valueMap, finding, component }));
    return;
  }

  if (renames) {
    edits.push({ start: attr.name.start, end: attr.name.end, replacement: mapping.canonicalProp });
  }
  edits.push({ start: literal.start, end: literal.end, replacement: JSON.stringify(mapped) });
  changes.push({
    kind: "rename-prop",
    ruleId: finding.ruleId,
    code: finding.code,
    severity: levelToSeverity(finding.level),
    message: renames
      ? `Renamed ${mapping.rawProp} to ${mapping.canonicalProp} and mapped value "${literal.value}" to "${mapped}" for <${component}>.`
      : `Mapped ${mapping.rawProp} value "${literal.value}" to "${mapped}" for <${component}>.`,
    before: `${mapping.rawProp}="${literal.value}"`,
    after: `${mapping.canonicalProp}="${mapped}"`,
    canonical: component,
    confidence: "high",
    location: toLocation(finding.location),
  });
}

function mapValueSuggestion(args: {
  mapping: ConformPropMapping;
  valueMap: Record<string, string>;
  finding: Finding;
  component: string;
}): ConformSuggestion {
  const { mapping, valueMap, finding, component } = args;
  const known = Object.entries(valueMap)
    .map(([from, to]) => `${from} → ${to}`)
    .join(", ");
  return {
    kind: "map-prop-value",
    message: `Map the ${mapping.rawProp} value on <${component}> to a confirmed ${mapping.canonicalProp} value.`,
    canonical: component,
    hint: known ? `Confirmed values: ${known}` : undefined,
    confidence: "medium",
    location: toLocation(finding.location),
  };
}

function findElementForPropRename(args: {
  finding: Finding;
  component: string;
  mappings: readonly ConformPropMapping[];
  elementsByLocation: ReadonlyMap<string, ElementCandidate>;
  candidatesByComponentLine: ReadonlyMap<string, readonly ElementCandidate[]>;
  usedElements: ReadonlySet<string>;
}): ElementCandidate | null {
  const key = `${args.finding.location.line}:${args.finding.location.column}`;
  const exact = args.elementsByLocation.get(key);
  if (
    exact &&
    !args.usedElements.has(exact.key) &&
    jsxNameMatches(exact.path.node.openingElement.name, args.component) &&
    hasAnyRawProp(exact.path, args.mappings)
  ) {
    return exact;
  }

  const candidates =
    args.candidatesByComponentLine.get(
      componentLineKey(args.component, args.finding.location.line)
    ) ?? [];
  return (
    candidates
      .filter(
        (candidate) =>
          !args.usedElements.has(candidate.key) && hasAnyRawProp(candidate.path, args.mappings)
      )
      .sort(
        (a, b) =>
          Math.abs(a.column - args.finding.location.column) -
          Math.abs(b.column - args.finding.location.column)
      )[0] ?? null
  );
}

function ensureNamedImports(
  source: string,
  findings: readonly Finding[]
): { source: string; changes: ConformChange[]; importsAdded: string[] } {
  const imports = uniqueImportRequests(findings);
  if (imports.length === 0) return { source, changes: [], importsAdded: [] };

  let next = source;
  const changes: ConformChange[] = [];
  const importsAdded: string[] = [];
  for (const request of imports) {
    const ensured = ensureNamedImport(next, request.importPath, request.component);
    next = ensured.source;
    if (!ensured.added) continue;
    importsAdded.push(ensured.statement);
    changes.push({
      kind: "rewrite-import",
      ruleId: request.finding.ruleId,
      code: request.finding.code,
      severity: levelToSeverity(request.finding.level),
      message: `Imported ${request.component} from ${request.importPath}.`,
      after: ensured.statement,
      canonical: request.component,
      confidence: "high",
      location: toLocation(request.finding.location),
    });
  }
  return { source: next, changes, importsAdded };
}

function ensureNamedImport(
  source: string,
  importPath: string,
  component: string
): { source: string; added: boolean; statement: string } {
  const statement = `import { ${component} } from "${importPath}";`;
  const ast = parseSafely(source);
  if (!ast) return { source: `${statement}\n${source}`, added: true, statement };

  const imports: Array<NodePath<t.ImportDeclaration>> = [];
  traverse(ast, {
    ImportDeclaration(path) {
      imports.push(path);
    },
  });

  for (const path of imports) {
    if (path.node.source.value !== importPath) continue;
    if (path.node.specifiers.some((s) => importSpecifierMatches(s, component))) {
      return { source, added: false, statement };
    }
    const named = path.node.specifiers.filter(t.isImportSpecifier);
    if (named.length > 0) {
      const last = named[named.length - 1];
      if (typeof last.end !== "number") break;
      return {
        source: `${source.slice(0, last.end)}, ${component}${source.slice(last.end)}`,
        added: true,
        statement,
      };
    }
  }

  const insertAt = imports.length > 0 ? imports[imports.length - 1].node.end : 0;
  if (typeof insertAt !== "number") {
    return { source: `${statement}\n${source}`, added: true, statement };
  }
  const prefix = insertAt === 0 ? "" : "\n";
  return {
    source: `${source.slice(0, insertAt)}${prefix}${statement}\n${source.slice(insertAt)}`,
    added: true,
    statement,
  };
}

function isComponentFinding(finding: Finding): boolean {
  return finding.ruleId === "components/prefer-library" && Boolean(finding.fix);
}

function parseSafely(source: string): t.File | null {
  try {
    return parse(source, PARSER_OPTIONS);
  } catch {
    return null;
  }
}

function uniqueImportRequests(findings: readonly Finding[]) {
  const seen = new Set<string>();
  const out: Array<{ component: string; importPath: string; finding: Finding }> = [];
  for (const finding of findings) {
    const component = stringAttr(finding.attributes, "suggestedComponent");
    const importPath = stringAttr(finding.attributes, "suggestedImport");
    if (!component || !importPath || !isIdentifier(component)) continue;
    const key = `${component}\0${importPath}`;
    if (seen.has(key)) continue;
    seen.add(key);
    out.push({ component, importPath, finding });
  }
  return out;
}

function importSpecifierMatches(
  specifier: t.ImportDeclaration["specifiers"][number],
  name: string
) {
  if (t.isImportSpecifier(specifier)) {
    const imported = specifier.imported;
    if (t.isIdentifier(imported) && imported.name === name) return true;
    if (t.isStringLiteral(imported) && imported.value === name) return true;
    return specifier.local.name === name;
  }
  return specifier.local.name === name;
}

function jsxNameMatches(name: t.JSXElement["openingElement"]["name"], expected: string) {
  return t.isJSXIdentifier(name) && name.name === expected;
}

function jsxIdentifierName(name: t.JSXElement["openingElement"]["name"]): string | null {
  return t.isJSXIdentifier(name) ? name.name : null;
}

function componentLineKey(component: string, line: number): string {
  return `${component}\0${line}`;
}

function hasAnyRawProp(path: NodePath<t.JSXElement>, mappings: readonly ConformPropMapping[]) {
  return mappings.some((mapping) => {
    if (!isIdentifier(mapping.rawProp)) return false;
    if (!findJsxAttribute(path, mapping.rawProp)) return false;
    return mapping.rawProp !== mapping.canonicalProp || Boolean(usableValueMap(mapping));
  });
}

function usableValueMap(mapping: ConformPropMapping): Record<string, string> | null {
  const map = mapping.valueMap;
  if (!map || typeof map !== "object") return null;
  return Object.keys(map).length > 0 ? map : null;
}

function stringLiteralValue(
  attr: t.JSXAttribute
): { value: string; start: number; end: number } | null {
  const value = attr.value;
  if (
    value &&
    t.isStringLiteral(value) &&
    typeof value.start === "number" &&
    typeof value.end === "number"
  ) {
    return { value: value.value, start: value.start, end: value.end };
  }
  return null;
}

function findJsxAttribute(path: NodePath<t.JSXElement>, name: string): t.JSXAttribute | null {
  for (const attr of path.node.openingElement.attributes) {
    if (!t.isJSXAttribute(attr)) continue;
    if (t.isJSXIdentifier(attr.name) && attr.name.name === name) return attr;
  }
  return null;
}

function propMappings(attributes: Finding["attributes"]): ConformPropMapping[] {
  const value = attributes?.propMapping;
  if (!Array.isArray(value)) return [];
  return value.filter(isPropMapping);
}

function isPropMapping(value: unknown): value is ConformPropMapping {
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
  const record = value as Record<string, unknown>;
  return (
    typeof record.rawProp === "string" &&
    typeof record.canonicalProp === "string" &&
    (record.valueMap === undefined || isStringRecord(record.valueMap))
  );
}

function isStringRecord(value: unknown): value is Record<string, string> {
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
  return Object.values(value as Record<string, unknown>).every((v) => typeof v === "string");
}

function applyEdits(source: string, edits: readonly Edit[]): string {
  let next = source;
  for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
    next = next.slice(0, edit.start) + edit.replacement + next.slice(edit.end);
  }
  return next;
}

function stringAttr(
  attributes: Record<string, unknown> | undefined,
  key: string
): string | undefined {
  const value = attributes?.[key];
  return typeof value === "string" && value.trim() ? value : undefined;
}

function levelToSeverity(level: unknown): "error" | "warning" | "info" {
  if (level === "error") return "error";
  if (level === "warn") return "warning";
  return "info";
}

function toLocation(location: Finding["location"]) {
  return {
    line: location.line,
    column: location.column,
    endLine: location.endLine,
    endColumn: location.endColumn,
  };
}

function isIdentifier(value: string): boolean {
  return /^[A-Za-z_$][\w$]*$/.test(value);
}
