import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import ts from "typescript";
import { findDuplicateTranslationKeys } from "./i18n-catalog-validation.mjs";

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const webuiDir = path.resolve(scriptDir, "..");
const workspaceRoot = path.resolve(webuiDir, "..", "..");

const sourceRoots = fs.readdirSync(workspaceRoot, { withFileTypes: true })
  .filter((entry) => entry.isDirectory() && entry.name.startsWith("govoplan-"))
  .map((entry) => path.join(workspaceRoot, entry.name, "webui", "src"))
  .filter((sourceRoot) => fs.existsSync(sourceRoot));

const generatedCatalogs = sourceRoots
  .map((sourceRoot) => path.join(sourceRoot, "i18n", "generatedTranslations.ts"))
  .filter((file) => fs.existsSync(file));

function scanStructuralSourcePositions(roots) {
  const files = roots.flatMap((root) => rgFiles(root).filter((file) => /\.(tsx?|jsx?)$/.test(file) && !file.endsWith("/i18n/generatedTranslations.ts")));
  const findings = [];
  for (const file of files) {
    const source = ts.createSourceFile(file, fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);

    function report(node, label) {
      const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
      findings.push(`${file}:${line + 1}:${character + 1} ${label} contains an i18n marker`);
    }

    function visit(node) {
      if (isStringLiteralLike(node) && node.text.includes("i18n:") && isStructuralStringPosition(node, file) && !isAllowedStructuralStringPosition(node, file)) {
        report(node, "structural string");
      }
      if (ts.isJsxAttribute(node)) {
        const name = nodeName(node.name);
        if ((isStructuralName(name) || isStructuralJsxValue(node)) && hasI18nLiteral(node.initializer)) report(node, `JSX ${name}`);
      }
      if (ts.isPropertyAssignment(node)) {
        const name = nodeName(node.name);
        if ((isStructuralName(name) || isAlgorithmNameProperty(node) || isListOptionValueProperty(node)) && hasI18nLiteral(node.initializer)) report(node, `object ${name}`);
        if (hasI18nLiteral(node.name) && !isAllowedStructuralStringPosition(node.name, file)) report(node, "object key");
      }
      if (ts.isShorthandPropertyAssignment(node) && node.name.text.includes("i18n:")) report(node, "object key");
      if (ts.isVariableDeclaration(node)) {
        const name = nodeName(node.name);
        if (isStructuralVariableName(name) && name !== "LEGACY_STORAGE_KEYS" && hasI18nLiteral(node.initializer)) report(node, `var ${name}`);
      }
      if (ts.isCallExpression(node) && isStructuralCall(node) && hasI18nLiteral(node.arguments[0])) {
        report(node.arguments[0], `call ${node.expression.getText(source)}`);
      }
      if (ts.isCallExpression(node)) {
        node.arguments.forEach((argument, index) => {
          if (isStructuralCallArgument(node, index) && hasI18nLiteral(argument)) {
            report(argument, `call ${node.expression.getText(source)} argument ${index + 1}`);
          }
        });
      }
      if (ts.isNewExpression(node) && node.expression.getText(source) === "CustomEvent" && hasI18nLiteral(node.arguments?.[0])) {
        report(node.arguments[0], "new CustomEvent");
      }
      if (ts.isArrayLiteralExpression(node) && hasI18nLiteral(node) && isLookupArray(node)) {
        report(node, "lookup array");
      }
      ts.forEachChild(node, visit);
    }

    visit(source);
  }
  return findings;
}

function scanGeneratedCatalogs(files) {
  const findings = [];
  for (const file of files) {
    const source = fs.readFileSync(file, "utf8");
    findings.push(...findDuplicateTranslationKeys(source, file));
    const translations = loadGeneratedTranslations(file);
    for (const [language, dictionary] of Object.entries(translations)) {
      for (const [key, value] of Object.entries(dictionary)) {
        if (looksStructuralCatalogValue(String(value), key)) {
          findings.push(`${file} ${language} ${key} has structural value ${JSON.stringify(value)}`);
        }
      }
    }
  }
  return findings;
}

function rgFiles(root) {
  try {
    const output = execFileSync("rg", ["--files", root], { encoding: "utf8" }).trim();
    return output ? output.split("\n") : [];
  } catch {
    return [];
  }
}

function nodeName(name) {
  if (!name) return "";
  if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
  return name.getText();
}

function isStructuralName(name) {
  if (!name) return false;
  if (name === "aria-label" || name === "alt" || name === "placeholder" || name === "title") return false;
  if (name.startsWith("data-")) return true;
  return structuralNames.has(name) ||
    name.endsWith("ClassName") ||
    /(?:^|_)(?:id|ids|key|keys|slug|code|path|url|endpoint|route|scope|permission|capability|module|event|storage)(?:_|$)/i.test(name) ||
    /(?:Id|IDs|Key|Keys|Slug|Code|Path|Url|URL|Endpoint|Route|Scope|Permission|Capability|Module|Event|Storage)$/.test(name);
}

function hasI18nLiteral(node) {
  let found = false;
  function visit(child) {
    if (
      (ts.isStringLiteral(child) || ts.isNoSubstitutionTemplateLiteral(child) || ts.isTemplateHead(child) || ts.isTemplateMiddle(child) || ts.isTemplateTail(child)) &&
      child.text.includes("i18n:")
    ) {
      found = true;
    }
    if (!found) ts.forEachChild(child, visit);
  }
  if (node) visit(node);
  return found;
}

function isStringLiteralLike(node) {
  return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node);
}

function isStructuralStringPosition(node, file) {
  const parent = node.parent;
  if (!parent) return false;
  if (ts.isPropertyAssignment(parent) && parent.name === node) return true;
  if (ts.isElementAccessExpression(parent) && parent.argumentExpression === node) return true;
  if (ts.isLiteralTypeNode(parent)) return true;
  if (ts.isImportDeclaration(parent) || ts.isExportDeclaration(parent)) return true;
  if (ts.isImportTypeNode(parent)) return true;
  if (ts.isCallExpression(parent) && isStructuralCall(parent) && parent.arguments[0] === node) return true;
  if (ts.isNewExpression(parent) && parent.expression.getText() === "CustomEvent" && parent.arguments?.[0] === node) return true;
  return false;
}

function isAllowedStructuralStringPosition(node, file) {
  if (file.endsWith("/utils/fieldHelp.ts") && ts.isPropertyAssignment(node.parent) && node.parent.name === node) return true;
  return hasAncestorVariable(node, "LEGACY_STORAGE_KEYS");
}

function isStructuralVariableName(name) {
  return /(?:^|_)(?:CLASS|CLASSNAME|STYLE|ID|KEY|SLUG|CODE|PATH|URL|ENDPOINT|ROUTE|SCOPE|PERMISSION|CAPABILITY|EVENT|STORAGE|ALGORITHM)(?:_|$)/i.test(name) ||
    /(?:ClassName|Style|Id|ID|Key|Slug|Code|Path|Url|URL|Endpoint|Route|Scope|Permission|Capability|Event|Storage|Algorithm)$/.test(name);
}

function isStructuralCall(node) {
  const expression = node.expression.getText();
  return /(?:^|\.)(?:getItem|setItem|removeItem|addEventListener|removeEventListener|dispatchEvent)$/.test(expression);
}

function isStructuralCallArgument(node, index) {
  const expressionName = callExpressionName(node);
  if (isStructuralCall(node) && index === 0) return true;
  if ((expressionName === "metadataText" || expressionName === "metadataFlag" || expressionName === "getText" || expressionName === "getBool") && index === 1) return true;
  if (expressionName === "createAttachmentBasePath" && index === 0) return true;
  if (expressionName === "updateNested" && index === 1) return true;
  if ((expressionName === "hasScope" || expressionName === "hasAnyScope") && index === 1) return true;
  if ((expressionName === "usePlatformModuleInstalled" || expressionName === "usePlatformUiCapability") && index === 0) return true;
  return false;
}

function callExpressionName(node) {
  const expression = node.expression;
  if (ts.isIdentifier(expression)) return expression.text;
  if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
  return expression.getText();
}

function isLookupArray(node) {
  let parent = node.parent;
  while (parent && (ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent))) {
    parent = parent.parent;
  }
  return Boolean(
    parent &&
    ts.isPropertyAccessExpression(parent) &&
    parent.expression === node &&
    ["includes", "indexOf"].includes(parent.name.text) &&
    ts.isCallExpression(parent.parent)
  );
}

function isAlgorithmNameProperty(node) {
  if (nodeName(node.name) !== "name") return false;
  const objectLiteral = node.parent;
  if (!ts.isObjectLiteralExpression(objectLiteral)) return false;
  if (objectLiteral.properties.some((property) => ts.isPropertyAssignment(property) && ["hash", "namedCurve"].includes(nodeName(property.name)))) return true;
  let parent = objectLiteral.parent;
  while (parent && (ts.isConditionalExpression(parent) || ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent))) {
    parent = parent.parent;
  }
  if (ts.isVariableDeclaration(parent)) return /Algorithm$/i.test(nodeName(parent.name));
  if (ts.isPropertyAssignment(parent)) return /Algorithm$/i.test(nodeName(parent.name));
  return false;
}

function isListOptionValueProperty(node) {
  if (nodeName(node.name) !== "value") return false;
  const objectLiteral = node.parent;
  if (!ts.isObjectLiteralExpression(objectLiteral)) return false;
  return objectLiteral.properties.some((property) => ts.isPropertyAssignment(property) && nodeName(property.name) === "label");
}

function isStructuralJsxValue(node) {
  if (nodeName(node.name) !== "value") return false;
  const parent = node.parent;
  if (!ts.isJsxOpeningElement(parent) && !ts.isJsxSelfClosingElement(parent)) return false;
  return parent.tagName.getText() === "option";
}

function hasAncestorVariable(node, variableName) {
  let current = node.parent;
  while (current) {
    if (ts.isVariableDeclaration(current) && nodeName(current.name) === variableName) return true;
    current = current.parent;
  }
  return false;
}

const structuralNames = new Set([
  "action",
  "actionId",
  "algorithm",
  "allOf",
  "anyOf",
  "apiKey",
  "authMethod",
  "capability",
  "capabilityId",
  "class",
  "className",
  "code",
  "columnType",
  "data-testid",
  "endpoint",
  "event",
  "eventName",
  "eventType",
  "field",
  "for",
  "form",
  "hash",
  "href",
  "htmlFor",
  "iconName",
  "id",
  "kind",
  "key",
  "method",
  "moduleId",
  "nameAttribute",
  "namedCurve",
  "ownerModule",
  "owner_module",
  "path",
  "permission",
  "permissions",
  "rel",
  "requiredScopes",
  "role",
  "route",
  "scope",
  "scopeId",
  "scopeType",
  "slug",
  "status",
  "storageKey",
  "style",
  "target",
  "to",
  "tone",
  "type",
  "url",
  "variant"
]);

function loadGeneratedTranslations(file) {
  const source = fs.readFileSync(file, "utf8");
  const output = ts.transpileModule(source, {
    compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }
  }).outputText;
  const sandbox = { exports: {}, module: { exports: {} }, require: () => ({}) };
  vm.runInNewContext(output, sandbox, { filename: file });
  return sandbox.exports.generatedTranslations ?? sandbox.module.exports.generatedTranslations ?? {};
}

const cssUnits = /^(?:-?\d+(?:\.\d+)?(?:px|r?em|vh|vw|vmin|vmax|%|s|ms)?|0)(?:\s+(?:-?\d+(?:\.\d+)?(?:px|r?em|vh|vw|vmin|vmax|%|s|ms)?|0)){0,3}$/i;
const cssFunction = /^(?:minmax|repeat|calc|var|clamp|rgba?|hsla?)\(/i;
const color = /^(?:#[0-9a-f]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\))$/i;
const cssIdentifier = /^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/;
const emailAddress = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
const emailAddressList = /^[^@\s;,]+@[^@\s;,]+\.[^@\s;,]+(?:\s*[;,]\s*[^@\s;,]+@[^@\s;,]+\.[^@\s;,]+)*$/;
const urlLike = /^(?:https?|webcal):\/\//i;
const packageSpecifier = /^@[a-z0-9-]+\/[a-z0-9-]+$/i;
const shellCommand = /^(?:systemctl|pg_dump|pg_restore|npm|pnpm|uv|pip|python)\b/;
const wildcardDomain = /^\*\.[a-z0-9.-]+$/i;
const technicalDottedIdentifier = /^[a-z][a-z0-9]*(?:[._:-][a-z0-9][a-z0-9-]*)+$/;
const environmentReference = /^(?:env|secret):[A-Z0-9_]+$/;
const codeEllipsis = /^[a-z][a-z0-9_-]*-\.\.\.$/;
const globOrPathChoice = /^[\w./*-]+(?:\s+or\s+[\w./*-]+)+$/i;
const multiLineStructuralList = /^(?:[A-Za-z0-9_.:/-]+[*?]?(?:\n|$))+$/;
const cryptographicIdentifier = /^(?:ECDSA|Ed25519|RSASSA-PKCS1-v1_5|RSA-PSS|AES-GCM|SHA-\d+)$/;
const classStateTokens = new Set(["active", "subtle", "success", "neutral", "danger", "disabled", "compact", "hidden", "block", "inline"]);
const allowedStructuralLookingValues = new Set(["active tenant", "campaign-local settings", "govoplan-files"]);
const allowedStructuralLookingKeys = /(?:active_tenant|python_package_placeholder|manifest_signature_key|remote_entry|remote_manifest)/;

function looksStructuralCatalogValue(value, key) {
  const trimmed = value.trim();
  if (!trimmed || allowedStructuralLookingValues.has(trimmed) || allowedStructuralLookingKeys.test(key)) return false;
  if (emailAddress.test(trimmed) || emailAddressList.test(trimmed) || cryptographicIdentifier.test(trimmed)) return true;
  if (environmentReference.test(trimmed) || codeEllipsis.test(trimmed)) return true;
  if (globOrPathChoice.test(trimmed) && /[/*.]/.test(trimmed)) return true;
  if (urlLike.test(trimmed) || packageSpecifier.test(trimmed) || shellCommand.test(trimmed) || wildcardDomain.test(trimmed)) return true;
  if (technicalDottedIdentifier.test(trimmed)) return true;
  if (trimmed.includes("\n") && multiLineStructuralList.test(trimmed)) return true;
  if (cssUnits.test(trimmed) || cssFunction.test(trimmed) || color.test(trimmed)) return true;
  if (/^[.#][A-Za-z_][\w-]*(?:\s*[.#][A-Za-z_][\w-]*)*$/.test(trimmed)) return true;

  const tokens = trimmed.split(/\s+/);
  if (!tokens.every((token) => cssIdentifier.test(token))) return false;
  if (!tokens.some((token) => token.includes("-") || token.startsWith("is-") || token.startsWith("has-") || classStateTokens.has(token))) return false;
  if (value !== trimmed) return true;
  if (tokens.length > 1 && tokens.every((token) => token === token.toLowerCase()) && tokens.some((token) => token.includes("-") || token.startsWith("is-") || token.startsWith("has-"))) return true;
  if (tokens.length === 1 && (/^(?:is-|has-|with-)/.test(trimmed) || (trimmed.includes("-") && trimmed === trimmed.toLowerCase()))) return true;
  return false;
}

const sourceFindings = scanStructuralSourcePositions(sourceRoots);
const catalogFindings = scanGeneratedCatalogs(generatedCatalogs);
const findings = [...sourceFindings, ...catalogFindings];

if (findings.length) {
  console.error(findings.join("\n"));
  process.exit(1);
}

console.log("No structural i18n markers found in source positions or generated catalogs.");
