import type { MatrixCell } from "../../../schema/src/records/context.js";
import type { RenderedCellEvidence, RenderedViolationFact } from "./types.js";
import { INVALID_RENDER_TOKENS } from "./probe.js";

const I18N_KEY_PATTERN = /^[a-z][a-z0-9]*(?:[._:-][a-z0-9]+)+$/i;

const RTL_LOCALE_PREFIXES = ["ar", "he", "fa", "ur", "ps", "sd", "yi"];

function parseJsonArray(value: string | undefined, field: string): string[] {
  if (value === undefined || value.trim().length === 0) {
    return [];
  }
  let parsed: unknown;
  try {
    parsed = JSON.parse(value) as unknown;
  } catch {
    throw new Error(`${field} must be valid JSON array`);
  }
  if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
    throw new Error(`${field} must be a JSON string array`);
  }
  return parsed.filter((entry): entry is string => typeof entry === "string");
}

function expectedDirection(locale: string): "ltr" | "rtl" {
  const prefix = locale.split("-")[0]?.toLowerCase() ?? locale.toLowerCase();
  return RTL_LOCALE_PREFIXES.some((rtlPrefix) => prefix === rtlPrefix) ? "rtl" : "ltr";
}

function expectedLang(locale: string): string {
  return locale.split("-")[0]?.toLowerCase() ?? locale.toLowerCase();
}

function containsInvalidRenderToken(text: string): string | undefined {
  for (const token of INVALID_RENDER_TOKENS) {
    if (text.includes(token)) {
      return token;
    }
  }
  return undefined;
}

function isDeclaredLiteralKey(cell: MatrixCell, text: string): boolean {
  const literals = parseJsonArray(cell.state.uiLiteralI18nKeys, "state.uiLiteralI18nKeys");
  return literals.includes(text);
}

function isCatalogKey(cell: MatrixCell, text: string): boolean {
  const catalog = parseJsonArray(cell.state.uiI18nCatalog, "state.uiI18nCatalog");
  return catalog.includes(text);
}

function hasI18nCatalog(cell: MatrixCell): boolean {
  const catalog = cell.state.uiI18nCatalog;
  return catalog !== undefined && catalog.trim().length > 0;
}

function hasRequiredLandmarkContract(cell: MatrixCell): boolean {
  return cell.state.uiRequiredLandmarkConfirmed === "true";
}

function hasHydrationAdapter(cell: MatrixCell): boolean {
  const adapter = cell.state.uiHydrationAdapter;
  return adapter !== undefined && adapter.trim().length > 0 && adapter !== "none";
}

function overflowTolerance(cell: MatrixCell): number {
  const raw = cell.state.uiOverflowTolerance;
  if (raw === undefined || raw.trim().length === 0) {
    return 1;
  }
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed < 0) {
    throw new Error("state.uiOverflowTolerance must be a non-negative number");
  }
  return parsed;
}

function networkExpectationViolations(
  evidence: RenderedCellEvidence,
): RenderedViolationFact[] {
  const expectations = evidence.cell.expectedNetwork ?? [];
  const violations: RenderedViolationFact[] = [];

  for (const entry of evidence.network) {
    const status = entry.status ?? 0;
    const failed = status >= 400 || status === 0;
    if (!failed) {
      continue;
    }

    const matchingExpectation = expectations.find((expectation) => {
      if (entry.method.toUpperCase() !== expectation.method.toUpperCase()) {
        return false;
      }
      return entry.url.includes(expectation.urlPattern);
    });

    const declared = matchingExpectation !== undefined;
    const contradicts =
      declared &&
      matchingExpectation.status !== undefined &&
      matchingExpectation.status !== status;

    if (declared && !contradicts) {
      continue;
    }

    violations.push({
      ruleId: "UI-012",
      kind: declared ? "unexpected-request-failure" : "undeclared-request-failure",
      locator: entry.url,
      measurement: {
        method: entry.method,
        status,
        declared,
        contradicts,
      },
      summary: declared
        ? `Network response ${String(status)} contradicts declared expectation for ${entry.url}`
        : `Undeclared network failure ${String(status)} for ${entry.url}`,
    });
  }

  return violations;
}

export function detectInvalidRenderToken(
  evidence: RenderedCellEvidence,
): RenderedViolationFact[] {
  const violations: RenderedViolationFact[] = [];
  for (const observation of evidence.renderedProbe.visibleTexts) {
    const token = containsInvalidRenderToken(observation.normalizedText);
    if (token === undefined) {
      continue;
    }
    violations.push({
      ruleId: "UI-010",
      kind: "invalid-render-token",
      locator: observation.locator,
      measurement: {
        token,
        text: observation.text,
      },
      summary: `Visible text contains invalid render token "${token}"`,
    });
  }
  return violations;
}

function collapseRepeatedViolations(
  violations: RenderedViolationFact[],
): RenderedViolationFact[] {
  const collapsed = new Map<string, RenderedViolationFact>();
  const counts = new Map<string, number>();
  for (const violation of violations) {
    const key = `${violation.kind}::${violation.locator}::${String(violation.measurement.message)}`;
    counts.set(key, (counts.get(key) ?? 0) + 1);
    if (!collapsed.has(key)) {
      collapsed.set(key, violation);
    }
  }
  return [...collapsed.entries()].map(([key, violation]) => ({
    ...violation,
    measurement: { ...violation.measurement, occurrenceCount: counts.get(key) ?? 1 },
  }));
}

export function detectUncaughtConsoleError(
  evidence: RenderedCellEvidence,
): RenderedViolationFact[] {
  const violations: RenderedViolationFact[] = [];
  for (const entry of evidence.console) {
    if (entry.type !== "error") {
      continue;
    }
    violations.push({
      ruleId: "UI-011",
      kind: "uncaught-console-error",
      locator: entry.location?.url ?? evidence.cell.url,
      measurement: {
        message: entry.text,
        lineNumber: entry.location?.lineNumber ?? -1,
        columnNumber: entry.location?.columnNumber ?? -1,
        hasStack: entry.text.includes("\n") || entry.text.includes("at "),
      },
      summary: `Uncaught console error: ${entry.text}`,
    });
  }
  return collapseRepeatedViolations(violations);
}

export function detectUnexpectedRequestFailure(
  evidence: RenderedCellEvidence,
): RenderedViolationFact[] {
  return networkExpectationViolations(evidence);
}

export function detectRawI18nKey(evidence: RenderedCellEvidence): RenderedViolationFact[] {
  const violations: RenderedViolationFact[] = [];
  for (const observation of evidence.renderedProbe.visibleTexts) {
    const text = observation.normalizedText;
    if (!I18N_KEY_PATTERN.test(text)) {
      continue;
    }
    if (isDeclaredLiteralKey(evidence.cell, text)) {
      continue;
    }
    if (isCatalogKey(evidence.cell, text)) {
      continue;
    }
    violations.push({
      ruleId: "UI-013",
      kind: "raw-i18n-key",
      locator: observation.locator,
      measurement: {
        key: text,
        catalogLoaded: hasI18nCatalog(evidence.cell),
      },
      summary: `Visible raw i18n key "${text}"`,
    });
  }
  return violations;
}

export function detectHtmlLanguageDirection(
  evidence: RenderedCellEvidence,
): RenderedViolationFact[] {
  const expectedDir = expectedDirection(evidence.cell.locale);
  const expectedLanguage = expectedLang(evidence.cell.locale);
  const actualDir = evidence.renderedProbe.htmlDir.length > 0
    ? evidence.renderedProbe.htmlDir
    : "ltr";
  const actualLang = evidence.renderedProbe.htmlLang.split("-")[0]?.toLowerCase() ?? "";

  const violations: RenderedViolationFact[] = [];
  if (actualDir !== expectedDir) {
    violations.push({
      ruleId: "UI-014",
      kind: "html-direction-mismatch",
      locator: "html[dir]",
      measurement: {
        expectedDir,
        actualDir,
      },
      summary: `html dir "${actualDir}" contradicts locale ${evidence.cell.locale}`,
    });
  }
  if (actualLang.length > 0 && actualLang !== expectedLanguage) {
    violations.push({
      ruleId: "UI-014",
      kind: "html-language-mismatch",
      locator: "html[lang]",
      measurement: {
        expectedLang: expectedLanguage,
        actualLang,
      },
      summary: `html lang "${actualLang}" contradicts locale ${evidence.cell.locale}`,
    });
  }
  return violations;
}

export function detectRequiredContentEmpty(
  evidence: RenderedCellEvidence,
): RenderedViolationFact[] {
  const required = evidence.cell.state.uiRequiredLandmark;
  if (required === undefined || required.trim().length === 0) {
    return [];
  }

  const violations: RenderedViolationFact[] = [];
  for (const landmark of evidence.renderedProbe.landmarks) {
    if (landmark.role !== required && landmark.locator !== required) {
      continue;
    }
    if (landmark.hasAccessibleContent) {
      continue;
    }
    violations.push({
      ruleId: "UI-015",
      kind: "required-content-empty",
      locator: landmark.locator,
      measurement: {
        role: landmark.role,
        requiredLandmark: required,
      },
      summary: `Required landmark "${required}" has no accessible content`,
    });
  }
  return violations;
}

export function detectStuckLoadingState(
  evidence: RenderedCellEvidence,
): RenderedViolationFact[] {
  if (evidence.settled.signal !== "app") {
    return [];
  }

  const violations: RenderedViolationFact[] = [];
  for (const indicator of evidence.renderedProbe.loadingIndicators) {
    if (!indicator.visible) {
      continue;
    }
    violations.push({
      ruleId: "UI-016",
      kind: "stuck-loading-state",
      locator: indicator.locator,
      measurement: {
        ariaBusy: indicator.ariaBusy,
        completionSignal: evidence.settled.signal,
      },
      summary: `Loading indicator remains visible after completion signal`,
    });
  }
  return violations;
}

export function detectHorizontalPageOverflow(
  evidence: RenderedCellEvidence,
): RenderedViolationFact[] {
  const tolerance = overflowTolerance(evidence.cell);
  const { scrollWidth, clientWidth } = evidence.renderedProbe.documentScroll;
  if (scrollWidth <= clientWidth + tolerance) {
    return [];
  }

  return [
    {
      ruleId: "UI-017",
      kind: "horizontal-page-overflow",
      locator: "document",
      measurement: {
        scrollWidth,
        clientWidth,
        tolerance,
        intentional: evidence.cell.state.uiDocumentOverflowIntentional === "true",
      },
      summary: `Document scrollWidth ${String(scrollWidth)} exceeds clientWidth ${String(clientWidth)}`,
    },
  ];
}

export function detectHydrationError(
  evidence: RenderedCellEvidence,
): RenderedViolationFact[] {
  const violations: RenderedViolationFact[] = [];

  for (const diagnostic of evidence.renderedProbe.hydrationDiagnostics) {
    violations.push({
      ruleId: "UI-018",
      kind: "hydration-error",
      locator: diagnostic.locator ?? "document",
      measurement: {
        diagnosticKind: diagnostic.kind,
        message: diagnostic.message,
      },
      summary: `Hydration diagnostic: ${diagnostic.message}`,
    });
  }

  for (const entry of evidence.console) {
    if (!/hydration/i.test(entry.text)) {
      continue;
    }
    violations.push({
      ruleId: "UI-018",
      kind: "hydration-error",
      locator: entry.location?.url ?? evidence.cell.url,
      measurement: {
        diagnosticKind: "console",
        message: entry.text,
      },
      summary: `Hydration console diagnostic: ${entry.text}`,
    });
  }

  return collapseRepeatedViolations(violations);
}

export function i18nProofConditionMet(cell: MatrixCell): boolean {
  return hasI18nCatalog(cell);
}

export function requiredContentProofConditionMet(cell: MatrixCell): boolean {
  return hasRequiredLandmarkContract(cell);
}

export function stuckLoadingProofConditionMet(evidence: RenderedCellEvidence): boolean {
  return evidence.settled.signal === "app";
}

export function hydrationProofConditionMet(cell: MatrixCell): boolean {
  return hasHydrationAdapter(cell);
}

export function overflowIntentAmbiguous(cell: MatrixCell): boolean {
  return cell.state.uiDocumentOverflowIntentional === "true";
}
