import { posix } from "node:path";
import ts from "typescript";
import { extractUcfsForReactStreaming, type UniversalComponentFact } from "../ucf/index.js";
import { normalizeScanSourcePath } from "./paths.js";
import type { ScanSource, ScanSourceFile } from "./types.js";

const REACT_SOURCE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".d.ts"];
const VIRTUAL_ROOT = "/__fragments_scan__";
const REACT_TYPES_SHIM_PATH = "node_modules/@types/react/index.d.ts";
const REACT_TYPES_SHIM = `declare module "react" {
  export type ReactNode = unknown;
  export type ReactElement = unknown;
  export interface FC<P = {}> {
    (props: P): ReactElement | null;
  }
  export type ComponentProps<T> = T extends (props: infer P) => unknown ? P : never;
  export type ComponentPropsWithoutRef<T> = ComponentProps<T>;
  export type ComponentPropsWithRef<T> = ComponentProps<T>;
}
`;

export interface ExtractReactUcfsFromSourceInput {
  source: ScanSource;
  files: readonly ScanSourceFile[];
  sourceCommit: string;
  adapterVersion?: string;
  tsconfigPath?: string;
  /**
   * Fired after each batch of source files is read so long-running scans
   * over large repos surface progress (and don't look stalled). Reads run
   * concurrently in batches of READ_CONCURRENCY rather than one-at-a-time.
   */
  onProgress?: (progress: { filesProcessed: number; totalFiles: number }) => void;
}

// Source files are fetched over the network (GitHub) one request each.
// Reading them concurrently in small batches keeps a large repo's component
// scan from taking minutes of serial round-trips. Kept modest to stay clear
// of GitHub's secondary (concurrent-request) rate limits.
const READ_CONCURRENCY = 8;

// Both the syntax-diagnostics pass and UCF extraction are otherwise-synchronous
// TypeScript work. On a large repo they can run long enough to starve a host's
// liveness heartbeat (the cloud scanner's watchdog read this as a stall and
// killed live runs). Yielding every N files lets pending timers fire.
const SYNTAX_DIAGNOSTIC_YIELD_EVERY = 50;
const EXTRACTION_YIELD_EVERY = 25;

export type ReactUcfSourceDiagnosticCode =
  | "missing_file"
  | "invalid_path"
  | "unsupported_file"
  | "invalid_tsconfig"
  | "typescript_syntax"
  | "no_component_exports"
  | "prop_metadata_partial";

export interface ReactUcfSourceDiagnostic {
  code: ReactUcfSourceDiagnosticCode;
  severity: "info" | "warn";
  message: string;
  filePath?: string;
}

export interface ExtractReactUcfsFromSourceResult {
  ucfs: UniversalComponentFact[];
  diagnostics: ReactUcfSourceDiagnostic[];
}

export async function extractReactUcfsFromSource(
  input: ExtractReactUcfsFromSourceInput
): Promise<UniversalComponentFact[]> {
  return (await extractReactUcfsFromSourceWithDiagnostics(input)).ucfs;
}

export async function extractReactUcfsFromSourceWithDiagnostics(
  input: ExtractReactUcfsFromSourceInput
): Promise<ExtractReactUcfsFromSourceResult> {
  const inlineSources: { filePath: string; source: string }[] = [];
  const diagnostics: ReactUcfSourceDiagnostic[] = [];

  // Validate paths up front (cheap, synchronous) so the concurrent read
  // phase only fetches files we actually intend to parse.
  const readablePaths: string[] = [];
  for (const file of input.files) {
    const relPath = normalizeScanSourcePath(file.relPath);
    if (!relPath) {
      diagnostics.push({
        code: "invalid_path",
        severity: "warn",
        message: `Invalid source path: ${file.relPath}`,
        filePath: file.relPath,
      });
      continue;
    }
    if (!isReactSourcePath(relPath)) {
      diagnostics.push({
        code: "unsupported_file",
        severity: "info",
        message: `Unsupported React source extension: ${relPath}`,
        filePath: relPath,
      });
      continue;
    }
    readablePaths.push(relPath);
  }

  const totalFiles = readablePaths.length;
  let filesProcessed = 0;
  input.onProgress?.({ filesProcessed, totalFiles });

  for (let i = 0; i < readablePaths.length; i += READ_CONCURRENCY) {
    const batch = readablePaths.slice(i, i + READ_CONCURRENCY);
    const contents = await Promise.all(
      batch.map(async (relPath) => {
        try {
          return { relPath, content: await input.source.readFile(relPath) };
        } catch {
          return { relPath, content: null as string | null };
        }
      })
    );

    for (const { relPath, content } of contents) {
      if (content === null) {
        diagnostics.push({
          code: "missing_file",
          severity: "warn",
          message: `Unable to read source file: ${relPath}`,
          filePath: relPath,
        });
        continue;
      }
      inlineSources.push({ filePath: virtualPath(relPath), source: content });
    }

    filesProcessed += batch.length;
    input.onProgress?.({ filesProcessed, totalFiles });
  }

  if (inlineSources.length === 0) return { ucfs: [], diagnostics };
  ensureReactTypesShim(inlineSources);

  const compilerOptions = await loadCompilerOptions(input.source, input.tsconfigPath, diagnostics);
  const syntaxDiagnostics = await typescriptSyntaxDiagnostics(inlineSources);
  const syntaxFiles = new Set(
    syntaxDiagnostics
      .map((diagnostic) => diagnostic.filePath)
      .filter((filePath): filePath is string => Boolean(filePath))
  );
  diagnostics.push(...syntaxDiagnostics);

  const extractedUcfs = await extractUcfsForReactStreaming({
    inlineSources,
    compilerOptions,
    sourceCommit: input.sourceCommit,
    adapterVersion: input.adapterVersion,
    cwd: VIRTUAL_ROOT,
    yieldEveryNFiles: EXTRACTION_YIELD_EVERY,
    // Keep reporting through the (synchronous) extraction so the host's run
    // never looks idle. Reads already advanced filesProcessed to totalFiles;
    // hold it there (monotonic) — this is a heartbeat, not a second count.
    onProgress: () => input.onProgress?.({ filesProcessed: totalFiles, totalFiles }),
  });
  const ucfs = extractedUcfs.filter((ucf) => !syntaxFiles.has(ucf.filePath));

  diagnostics.push(...propMetadataDiagnostics(ucfs));

  for (const source of inlineSources) {
    const relPath = source.filePath.slice(`${VIRTUAL_ROOT}/`.length);
    if (!isComponentCandidatePath(relPath)) continue;
    if (!ucfs.some((ucf) => ucf.filePath === relPath)) {
      diagnostics.push({
        code: "no_component_exports",
        severity: "info",
        message: `No exported React components found in ${relPath}`,
        filePath: relPath,
      });
    }
  }

  return { ucfs, diagnostics };
}

function propMetadataDiagnostics(
  ucfs: readonly UniversalComponentFact[]
): ReactUcfSourceDiagnostic[] {
  return ucfs.flatMap((ucf) => {
    const partialProps = ucf.props.filter((prop) => prop.schemaConfidence === "partial");
    if (partialProps.length === 0) return [];
    return [
      {
        code: "prop_metadata_partial" as const,
        severity: "info" as const,
        message: `Prop metadata for ${ucf.componentName} is partial; strict unknown-prop enforcement should not use it as a closed schema.`,
        filePath: ucf.filePath,
      },
    ];
  });
}

async function loadCompilerOptions(
  source: ScanSource,
  tsconfigPath = "tsconfig.json",
  diagnostics: ReactUcfSourceDiagnostic[]
): Promise<ts.CompilerOptions> {
  const relPath = normalizeScanSourcePath(tsconfigPath) ?? "tsconfig.json";
  const content = await source.readFile(relPath);
  if (!content) return defaultCompilerOptions();

  const parsed = ts.parseConfigFileTextToJson(relPath, content);
  if (parsed.error) {
    diagnostics.push({
      code: "invalid_tsconfig",
      severity: "warn",
      message: formatDiagnostic(parsed.error),
      filePath: relPath,
    });
  }
  if (!parsed.config || typeof parsed.config !== "object") return defaultCompilerOptions();

  const tsconfigDir = posix.dirname(relPath) === "." ? "." : posix.dirname(relPath);
  const basePath = tsconfigDir === "." ? VIRTUAL_ROOT : virtualPath(tsconfigDir);
  const converted = ts.convertCompilerOptionsFromJson(
    (parsed.config as { compilerOptions?: unknown }).compilerOptions ?? {},
    basePath,
    relPath
  );
  diagnostics.push(
    ...converted.errors.map((error) => ({
      code: "invalid_tsconfig" as const,
      severity: "warn" as const,
      message: formatDiagnostic(error),
      filePath: relPath,
    }))
  );

  return {
    ...defaultCompilerOptions(),
    ...converted.options,
  };
}

function defaultCompilerOptions(): ts.CompilerOptions {
  return {
    target: ts.ScriptTarget.ES2022,
    module: ts.ModuleKind.ESNext,
    moduleResolution: ts.ModuleResolutionKind.Bundler,
    jsx: ts.JsxEmit.ReactJSX,
    allowJs: true,
    allowSyntheticDefaultImports: true,
    esModuleInterop: true,
    skipLibCheck: true,
    strict: false,
    noEmit: true,
    rootDir: VIRTUAL_ROOT,
  };
}

function isReactSourcePath(path: string): boolean {
  return REACT_SOURCE_EXTENSIONS.some((ext) => path.endsWith(ext));
}

function isComponentCandidatePath(path: string): boolean {
  return path.endsWith(".tsx") || path.endsWith(".jsx");
}

function virtualPath(relPath: string): string {
  return `${VIRTUAL_ROOT}/${relPath}`;
}

function ensureReactTypesShim(inlineSources: Array<{ filePath: string; source: string }>): void {
  const shimPath = virtualPath(REACT_TYPES_SHIM_PATH);
  if (inlineSources.some((source) => source.filePath === shimPath)) return;
  inlineSources.push({ filePath: shimPath, source: REACT_TYPES_SHIM });
}

async function typescriptSyntaxDiagnostics(
  inlineSources: ReadonlyArray<{ filePath: string; source: string }>
): Promise<ReactUcfSourceDiagnostic[]> {
  const out: ReactUcfSourceDiagnostic[] = [];
  for (let i = 0; i < inlineSources.length; i += 1) {
    const source = inlineSources[i]!;
    const relPath = source.filePath.slice(`${VIRTUAL_ROOT}/`.length);
    const sourceFile = ts.createSourceFile(
      source.filePath,
      source.source,
      ts.ScriptTarget.Latest,
      true,
      source.filePath.endsWith(".tsx") || source.filePath.endsWith(".jsx")
        ? ts.ScriptKind.TSX
        : ts.ScriptKind.TS
    );
    const parseDiagnostics =
      (sourceFile as ts.SourceFile & { parseDiagnostics?: readonly ts.Diagnostic[] })
        .parseDiagnostics ?? [];
    for (const diagnostic of parseDiagnostics) {
      out.push({
        code: "typescript_syntax",
        severity: "warn",
        message: formatDiagnostic(diagnostic),
        filePath: relPath,
      });
    }
    if ((i + 1) % SYNTAX_DIAGNOSTIC_YIELD_EVERY === 0) {
      await new Promise((resolve) => setTimeout(resolve, 0));
    }
  }
  return out;
}

function formatDiagnostic(diagnostic: ts.Diagnostic): string {
  return ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
}
