import fs from "fs";
import path from "path";

const CLASSIFICATIONS = new Set([
  "expected_failure",
  "infra",
  "product_bug",
  "stale_test",
  "test_bug",
]);
const STATES = new Set(["closed", "open"]);

export function loadKnownFailuresConfig(productDir, config) {
  const relativePath = config?.knownFailuresFile;
  if (!relativePath) return null;

  const absolutePath = path.resolve(productDir, relativePath);
  if (!fs.existsSync(absolutePath)) {
    throw new Error(`Known failures file not found: ${relativePath}`);
  }

  return loadKnownFailuresDocument(absolutePath, relativePath);
}

export function loadKnownFailuresDocument(filePath, relativePath = filePath) {
  let parsed;
  try {
    parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
  } catch (error) {
    throw new Error(
      `Could not parse known failures file ${relativePath}: ${formatErrorMessage(error)}`
    );
  }

  return normalizeKnownFailuresDocument(parsed, relativePath);
}

export function normalizeKnownFailuresDocument(document, relativePath = "known failures file") {
  if (!document || typeof document !== "object") {
    throw new Error(`${relativePath} must contain a JSON object`);
  }
  if (document.schemaVersion !== 1) {
    throw new Error(`${relativePath} schemaVersion must be 1`);
  }
  if (!Array.isArray(document.entries)) {
    throw new Error(`${relativePath} entries must be an array`);
  }

  const ids = new Set();
  const entries = document.entries.map((entry, index) => {
    const normalized = normalizeKnownFailureEntry(entry, `${relativePath} entries[${index}]`);
    if (ids.has(normalized.id)) {
      throw new Error(`${relativePath} has duplicate entry id "${normalized.id}"`);
    }
    ids.add(normalized.id);
    return normalized;
  });

  return {
    schemaVersion: 1,
    issueRepo: normalizeOptionalString(document.issueRepo),
    entries,
  };
}

export function validateKnownFailuresDocument(
  document,
  { productDir, statusArtifactPath, statusArtifact } = {}
) {
  const errors = [];
  const warnings = [];
  const matchKeys = new Set();
  let matchCount = 0;

  for (const entry of document.entries) {
    for (const match of entry.matches) {
      matchCount += 1;
      if (productDir) {
        const absolutePath = path.join(productDir, match.path);
        if (!fs.existsSync(absolutePath)) {
          errors.push(`Missing matched file: ${match.path} (${entry.id})`);
        }
      }

      const matchKey = [
        entry.id,
        match.service || "",
        match.type || "",
        match.path,
        match.failureKey || "",
        match.errorIncludes || "",
      ].join("::");
      if (matchKeys.has(matchKey)) {
        errors.push(`Duplicate match selector in ${entry.id}: ${match.path}`);
      }
      matchKeys.add(matchKey);

      if (!match.path.includes("__testkit__") || !match.path.endsWith(".testkit.ts")) {
        warnings.push(`Match path does not look like a testkit file: ${match.path} (${entry.id})`);
      }
    }

    if (document.issueRepo && entry.issue.repo !== document.issueRepo) {
      warnings.push(
        `Entry ${entry.id} uses issue repo ${entry.issue.repo} instead of document issueRepo ${document.issueRepo}`
      );
    }
    if (!entry.issue.url.endsWith(`/issues/${entry.issue.number}`)) {
      errors.push(`Issue URL does not match issue number for ${entry.id}`);
    }
  }

  let failedTests = 0;
  let triagedFailedTests = 0;
  let untriagedFailedTests = 0;
  const parsedStatusArtifact = statusArtifact
    ? parseStatusArtifact(statusArtifact)
    : statusArtifactPath && fs.existsSync(statusArtifactPath)
      ? parseStatusArtifact(JSON.parse(fs.readFileSync(statusArtifactPath, "utf8")))
      : { tests: [] };

  const failed = parsedStatusArtifact.tests.filter((test) => test.status === "failed");
  failedTests = failed.length;
  for (const test of failed) {
    if (findMatchingKnownFailureEntries(document, test).length > 0) {
      triagedFailedTests += 1;
    } else {
      untriagedFailedTests += 1;
      warnings.push(`Untriaged failed test: ${test.path}`);
    }
  }

  return {
    errors,
    warnings,
    stats: {
      entries: document.entries.length,
      matches: matchCount,
      failedTests,
      triagedFailedTests,
      untriagedFailedTests,
    },
  };
}

export function renderKnownFailuresMarkdown(document) {
  const lines = [
    "# Bourne Bugs",
    "",
    "Canonical source: `testkit.known-failures.json`",
    "",
    `Tracked bug classes: ${document.entries.length}`,
    `Tracked issue references: ${new Set(document.entries.map((entry) => entry.issue.number)).size}`,
    "",
  ];

  document.entries.forEach((entry, index) => {
    lines.push(
      `## ${index + 1}. ${entry.title} — [#${entry.issue.number}](${entry.issue.url})`,
      "",
      `- Classification: \`${entry.classification}\``,
      `- State: \`${entry.state}\``,
      `- Description: ${entry.description}`,
      `- Why failing: ${entry.whyFailing}`,
      `- Last reviewed: ${entry.lastReviewedAt}`,
      "- Matches:"
    );

    for (const match of entry.matches) {
      lines.push(
        `  - \`${match.path}\`${match.failureKey ? ` — \`${match.failureKey}\`` : ""}${
          match.errorIncludes ? ` — error contains \`${match.errorIncludes}\`` : ""
        }`
      );
    }

    lines.push("");
  });

  return `${lines.join("\n").trimEnd()}\n`;
}

export function findMatchingKnownFailureEntries(document, fileSummary) {
  return document.entries.filter((entry) => matchesKnownFailureEntry(entry, fileSummary));
}

export function matchesKnownFailureEntry(entry, fileSummary) {
  return entry.matches.some((match) => matchesKnownFailureMatch(match, fileSummary));
}

export function matchesKnownFailureMatch(match, fileSummary) {
  if (match.service && match.service !== fileSummary.service) return false;
  if (match.type && match.type !== fileSummary.type) return false;
  if (match.path !== fileSummary.path) return false;
  if (match.failureKey) {
    const failureKeys = Array.isArray(fileSummary.failureDetails)
      ? fileSummary.failureDetails.map((detail) => detail?.key)
      : [];
    if (!failureKeys.includes(match.failureKey)) return false;
  }
  if (match.errorIncludes && !String(fileSummary.error || "").includes(match.errorIncludes)) {
    return false;
  }
  return true;
}

export function buildKnownFailureFileIdentity(service, type, filePath) {
  return `${service}::${type}::${filePath}`;
}

function normalizeKnownFailureEntry(entry, label) {
  if (!entry || typeof entry !== "object") {
    throw new Error(`${label} must be an object`);
  }

  const id = requireNonEmptyString(entry.id, `${label}.id`);
  const title = requireNonEmptyString(entry.title, `${label}.title`);
  const classification = requireEnumValue(
    entry.classification,
    CLASSIFICATIONS,
    `${label}.classification`
  );
  const state = requireEnumValue(entry.state, STATES, `${label}.state`);
  const description = requireNonEmptyString(entry.description, `${label}.description`);
  const whyFailing = requireNonEmptyString(entry.whyFailing, `${label}.whyFailing`);
  const lastReviewedAt = requireNonEmptyString(entry.lastReviewedAt, `${label}.lastReviewedAt`);
  if (!Array.isArray(entry.matches) || entry.matches.length === 0) {
    throw new Error(`${label}.matches must be a non-empty array`);
  }

  return {
    id,
    title,
    classification,
    state,
    issue: normalizeKnownFailureIssue(entry.issue, `${label}.issue`),
    description,
    whyFailing,
    lastReviewedAt,
    matches: entry.matches.map((match, index) =>
      normalizeKnownFailureMatch(match, `${label}.matches[${index}]`)
    ),
  };
}

function normalizeKnownFailureIssue(issue, label) {
  if (!issue || typeof issue !== "object") {
    throw new Error(`${label} must be an object`);
  }

  const repo = requireNonEmptyString(issue.repo, `${label}.repo`);
  const number = issue.number;
  if (!Number.isInteger(number) || number <= 0) {
    throw new Error(`${label}.number must be a positive integer`);
  }
  const url = requireNonEmptyString(issue.url, `${label}.url`);

  return { repo, number, url };
}

function normalizeKnownFailureMatch(match, label) {
  if (!match || typeof match !== "object") {
    throw new Error(`${label} must be an object`);
  }

  const pathValue = requireNonEmptyString(match.path, `${label}.path`);
  const normalized = {
    path: pathValue,
  };

  const service = normalizeOptionalString(match.service);
  if (service) normalized.service = service;

  const type = normalizeOptionalString(match.type);
  if (type) normalized.type = type;

  const failureKey = normalizeOptionalString(match.failureKey);
  if (failureKey) normalized.failureKey = failureKey;

  const errorIncludes = normalizeOptionalString(match.errorIncludes);
  if (errorIncludes) normalized.errorIncludes = errorIncludes;

  return normalized;
}

function parseStatusArtifact(value) {
  if (!value || typeof value !== "object") {
    return { tests: [] };
  }

  return {
    tests: Array.isArray(value.tests) ? value.tests : [],
  };
}

function requireNonEmptyString(value, label) {
  const normalized = normalizeOptionalString(value);
  if (!normalized) {
    throw new Error(`${label} must be a non-empty string`);
  }
  return normalized;
}

function requireEnumValue(value, allowed, label) {
  const normalized = requireNonEmptyString(value, label);
  if (!allowed.has(normalized)) {
    throw new Error(`${label} must be one of: ${[...allowed].sort().join(", ")}`);
  }
  return normalized;
}

function normalizeOptionalString(value) {
  if (typeof value !== "string") return null;
  const normalized = value.trim();
  return normalized.length > 0 ? normalized : null;
}

function formatErrorMessage(error) {
  if (error instanceof Error) return error.message;
  return String(error);
}

export {
  buildKnownFailureIssueValidationSummaryLines,
  normalizeKnownFailureIssueValidationConfig,
  parseGitHubRepoSlug,
  shouldFailKnownFailureIssueValidation,
  validateKnownFailureIssues,
} from "./github.mjs";
