import fs from "node:fs/promises";
import path from "node:path";
import fg from "fast-glob";
import { type CommandResult, success } from "../../../lib/command-result";
import { APP_PATHS } from "../../../lib/paths";

export interface ResolverMeasure {
  count: number;
  avgLoC: number;
  maxLoC: number;
  filesOverThreshold: { path: string; loc: number }[];
}

export interface CommandMeasure {
  count: number;
  avgLoC: number;
  handWrittenRatio: number;
  libNonTypesFiles: string[];
}

export interface FrontendMeasure {
  pages: { count: number };
  jsxComposition: {
    appShellTags: number;
    rawHtmlTags: number;
    otherLibraryTags: number;
    appShellRatio: number;
  };
  rawHtmlHeavyPages: { path: string; appShellRatio: number; rawHtmlTags: number }[];
  customComponents: number;
}

export interface MeasureThinResult {
  resolvers?: ResolverMeasure;
  commands?: CommandMeasure;
  frontend?: FrontendMeasure;
}

const RESOLVER_LOC_THRESHOLD = 200;
const APP_SHELL_PACKAGE = "@tailor-platform/app-shell";
const RAW_HTML_HEAVY_RATIO = 0.5;
const RAW_HTML_HEAVY_MIN_TAGS = 5;

const IMPORT_NAMED_RE = /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
const JSX_TAG_OPEN_RE = /<([A-Za-z][A-Za-z0-9_.]*)/g;

function extractNamedIdentifiers(clause: string): string[] {
  return clause
    .split(",")
    .map((entry) => entry.trim())
    .filter(Boolean)
    .map((entry) => {
      const parts = entry.split(/\s+as\s+/);
      return parts[parts.length - 1].trim();
    })
    .filter(Boolean);
}

function parseImports(content: string): {
  appShell: Set<string>;
  otherLibrary: Set<string>;
} {
  const appShell = new Set<string>();
  const otherLibrary = new Set<string>();
  let m: RegExpExecArray | null;
  IMPORT_NAMED_RE.lastIndex = 0;
  while ((m = IMPORT_NAMED_RE.exec(content)) !== null) {
    const from = m[2];
    const idents = extractNamedIdentifiers(m[1]);
    if (from === APP_SHELL_PACKAGE) {
      for (const id of idents) appShell.add(id);
    } else if (!from.startsWith(".") && from !== "react" && from !== "react-dom") {
      for (const id of idents) otherLibrary.add(id);
    }
  }
  return { appShell, otherLibrary };
}

interface FileJsxCount {
  appShellTags: number;
  rawHtmlTags: number;
  otherLibraryTags: number;
}

function countJsxTags(
  content: string,
  appShell: Set<string>,
  otherLibrary: Set<string>,
): FileJsxCount {
  let appShellTags = 0;
  let rawHtmlTags = 0;
  let otherLibraryTags = 0;
  let m: RegExpExecArray | null;
  JSX_TAG_OPEN_RE.lastIndex = 0;
  while ((m = JSX_TAG_OPEN_RE.exec(content)) !== null) {
    const tag = m[1];
    const topLevel = tag.split(".")[0];
    if (/^[a-z]/.test(topLevel)) {
      rawHtmlTags++;
    } else if (appShell.has(topLevel)) {
      appShellTags++;
    } else if (otherLibrary.has(topLevel)) {
      otherLibraryTags++;
    }
  }
  return { appShellTags, rawHtmlTags, otherLibraryTags };
}

async function measureJsxFile(absPath: string): Promise<FileJsxCount> {
  const content = await fs.readFile(absPath, "utf-8");
  const { appShell, otherLibrary } = parseImports(content);
  return countJsxTags(content, appShell, otherLibrary);
}

async function lineCount(absPath: string): Promise<number> {
  const content = await fs.readFile(absPath, "utf-8");
  return content.split("\n").length;
}

async function measureResolvers(appRoot: string, cwd: string): Promise<ResolverMeasure> {
  const files = await fg(`${appRoot}/${APP_PATHS.code.resolver}/*.ts`, {
    cwd,
    ignore: ["**/*.test.ts", "**/index.ts"],
  });
  if (files.length === 0) {
    return { count: 0, avgLoC: 0, maxLoC: 0, filesOverThreshold: [] };
  }
  const locs: { path: string; loc: number }[] = [];
  for (const f of files) {
    locs.push({ path: f, loc: await lineCount(path.join(cwd, f)) });
  }
  const total = locs.reduce((s, x) => s + x.loc, 0);
  const max = locs.reduce((m, x) => Math.max(m, x.loc), 0);
  return {
    count: locs.length,
    avgLoC: Math.round(total / locs.length),
    maxLoC: max,
    filesOverThreshold: locs.filter((x) => x.loc > RESOLVER_LOC_THRESHOLD),
  };
}

async function measureCommands(modulesRoot: string, cwd: string): Promise<CommandMeasure> {
  const allFiles = await fg(`${modulesRoot}/*/command/*.ts`, {
    cwd,
    ignore: ["**/*.test.ts", "**/index.ts"],
  });
  const handWritten = allFiles.filter((f) => !f.endsWith(".generated.ts"));
  const generated = allFiles.filter((f) => f.endsWith(".generated.ts"));

  let avgLoC = 0;
  if (handWritten.length > 0) {
    const locs = await Promise.all(handWritten.map((f) => lineCount(path.join(cwd, f))));
    avgLoC = Math.round(locs.reduce((s, x) => s + x, 0) / locs.length);
  }

  const totalFiles = handWritten.length + generated.length;
  const handWrittenRatio =
    totalFiles > 0 ? Math.round((handWritten.length / totalFiles) * 100) / 100 : 0;

  const libFiles = await fg(`${modulesRoot}/*/lib/*.ts`, {
    cwd,
    ignore: ["**/*.test.ts"],
  });
  const libNonTypesFiles = libFiles.filter((f) => {
    const basename = path.basename(f);
    // Allow: types.ts (hand-written types), *.generated.ts (regenerated),
    // `_*.ts` (= cross-module stubs / private helpers by convention).
    return (
      basename !== "types.ts" && !basename.endsWith(".generated.ts") && !basename.startsWith("_")
    );
  });

  return { count: handWritten.length, avgLoC, handWrittenRatio, libNonTypesFiles };
}

async function measureFrontend(appRoot: string, cwd: string): Promise<FrontendMeasure> {
  const pages = await fg(`${appRoot}/frontend/src/pages/**/*.tsx`, {
    cwd,
    ignore: ["**/*.test.tsx", "**/*.stories.tsx"],
  });

  let totalAppShell = 0;
  let totalRawHtml = 0;
  let totalOtherLib = 0;
  const rawHtmlHeavyPages: { path: string; appShellRatio: number; rawHtmlTags: number }[] = [];

  for (const p of pages) {
    const counts = await measureJsxFile(path.join(cwd, p));
    totalAppShell += counts.appShellTags;
    totalRawHtml += counts.rawHtmlTags;
    totalOtherLib += counts.otherLibraryTags;

    const fileTotal = counts.appShellTags + counts.rawHtmlTags + counts.otherLibraryTags;
    if (fileTotal > 0) {
      const fileRatio = counts.appShellTags / fileTotal;
      if (fileRatio < RAW_HTML_HEAVY_RATIO && counts.rawHtmlTags >= RAW_HTML_HEAVY_MIN_TAGS) {
        rawHtmlHeavyPages.push({
          path: p,
          appShellRatio: Math.round(fileRatio * 100) / 100,
          rawHtmlTags: counts.rawHtmlTags,
        });
      }
    }
  }

  const totalTags = totalAppShell + totalRawHtml + totalOtherLib;
  const appShellRatio = totalTags > 0 ? Math.round((totalAppShell / totalTags) * 100) / 100 : 0;

  const components = await fg(`${appRoot}/frontend/src/components/*.tsx`, { cwd });

  return {
    pages: { count: pages.length },
    jsxComposition: {
      appShellTags: totalAppShell,
      rawHtmlTags: totalRawHtml,
      otherLibraryTags: totalOtherLib,
      appShellRatio,
    },
    rawHtmlHeavyPages,
    customComponents: components.length,
  };
}

export async function collectMeasureThin(
  config: { appRoot?: string; modulesRoot?: string },
  cwd: string,
): Promise<MeasureThinResult> {
  const result: MeasureThinResult = {};
  if (config.appRoot) {
    result.resolvers = await measureResolvers(config.appRoot, cwd);
    result.frontend = await measureFrontend(config.appRoot, cwd);
  }
  if (config.modulesRoot) {
    result.commands = await measureCommands(config.modulesRoot, cwd);
  }
  return result;
}

export async function runInternalMeasureThin(
  config: { appRoot?: string; modulesRoot?: string },
  cwd: string,
): Promise<CommandResult> {
  const result = await collectMeasureThin(config, cwd);
  console.log(formatMeasureThinReport(result));
  return success();
}

function formatMeasureThinReport(result: MeasureThinResult): string {
  return JSON.stringify(result, null, 2);
}
