/**
 * Issue → CSS Selector Mapper
 * ───────────────────────────────
 * Maps TechnicalIssue records to one-or-more OverlayIssue entries, each
 * targeting a SPECIFIC DOM element on a specific page. A single
 * TechnicalIssue row often aggregates many affected elements (e.g. 20
 * broken links on one page share one DB row, each link is an item in the
 * `evidence` JSON array). This module explodes those evidence items so
 * the Site Tag overlay can highlight every broken element individually —
 * not just one badge per DB row.
 *
 * Element-level issues (broken link, broken image, missing alt) produce
 * one OverlayIssue per matching evidence item with a CSS selector that
 * uniquely targets that element.
 *
 * Page-level issues (missing meta description, missing H1, duplicate
 * title, etc.) produce a single OverlayIssue with `selector: null` —
 * those are rendered in the floating control panel, not pinned to a
 * specific DOM element.
 */

import type { Prisma } from "@prisma/client";

// ─── Types ──────────────────────────────────────────────────────────────────

/** A single piece of evidence attached to an issue (one broken link, one image, etc.) */
export interface IssueEvidenceItem {
  sourcePageUrl?: string;
  brokenHref?: string;
  anchorText?: string;
  selector?: string;
  xpath?: string;
  snippet?: string;
  statusCode?: number;
  imageUrl?: string;
  targetElement?: string;
  redirectChain?: string[];
  [key: string]: unknown;
}

/** A mapped issue ready for the overlay client */
export interface OverlayIssue {
  id: string;
  /** Short machine-friendly type slug derived from the issue title */
  type: string;
  severity: "critical" | "high" | "medium" | "low" | "info";
  category: string;
  title: string;
  message: string;
  recommendation: string;
  /** CSS selector for the target element — null for page-level issues */
  selector: string | null;
  /** Optional context for the tooltip */
  context?: {
    anchorText?: string;
    targetUrl?: string;
    statusCode?: number;
    snippet?: string;
    redirectChain?: string[];
  };
}

// ─── Type normalisation ─────────────────────────────────────────────────────

/** Map the human-readable issue title to a machine-friendly type slug. */
export function normaliseType(title: string): string {
  const lower = title.toLowerCase();
  if (lower.startsWith("broken internal link")) return "broken_internal_link";
  if (lower.startsWith("broken external link")) return "broken_external_link";
  if (lower.startsWith("http ") && lower.includes("error")) return "http_error";
  if (lower.includes("missing title")) return "missing_title";
  if (lower.includes("title tag too")) return "title_length";
  if (lower.includes("missing meta description")) return "missing_meta_description";
  if (lower.includes("meta description too")) return "meta_description_length";
  if (lower.includes("duplicate title")) return "duplicate_title";
  if (lower.includes("duplicate meta description")) return "duplicate_meta_description";
  if (lower.includes("missing h1")) return "missing_h1";
  if (lower.includes("multiple h1")) return "multiple_h1";
  if (lower.includes("heading level skipped")) return "heading_hierarchy";
  if (lower.includes("missing canonical")) return "missing_canonical";
  if (lower.includes("images missing alt") || lower.includes("image missing alt")) return "missing_alt_text";
  if (lower.includes("missing accessibility landmark")) return "missing_landmark";
  if (lower.includes("redirect chain")) return "redirect_chain";
  if (lower.includes("broken anchor") || lower.includes("broken fragment")) return "broken_fragment";
  if (lower.includes("orphan")) return "orphan_page";
  if (lower.includes("slow resource")) return "slow_resource";
  return "other";
}

// ─── Severity normalisation ─────────────────────────────────────────────────

function normaliseSeverity(raw: string): OverlayIssue["severity"] {
  const s = raw.toLowerCase();
  if (s === "critical" || s === "high" || s === "medium" || s === "low" || s === "info") {
    return s;
  }
  if (s === "error") return "critical";
  if (s === "warning") return "medium";
  return "info";
}

// ─── URL normalisation ──────────────────────────────────────────────────────

/**
 * Normalise a URL or path for comparison. Strips protocol, `www.`,
 * trailing slash, and lowercases the host. Used to match
 * `evidence.sourcePageUrl` against the page the user is currently on.
 */
export function normaliseUrl(input: string): string {
  if (!input) return "";
  try {
    const u = new URL(input);
    const host = u.hostname.replace(/^www\./, "").toLowerCase();
    const path = u.pathname.replace(/\/+$/, "") || "/";
    return host + path + (u.search || "");
  } catch {
    // relative path
    return input.replace(/\/+$/, "") || "/";
  }
}

/** Just the path portion, normalised. */
export function normalisePath(input: string): string {
  if (!input) return "/";
  try {
    const u = new URL(input);
    return u.pathname.replace(/\/+$/, "") || "/";
  } catch {
    const cleaned = input.split("?")[0].split("#")[0];
    return cleaned.replace(/\/+$/, "") || "/";
  }
}

// ─── Selector builders ──────────────────────────────────────────────────────

/** Escape characters that would break a CSS attribute selector. */
function escapeAttr(value: string): string {
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}

/**
 * Build a CSS selector targeting the specific element described by one
 * evidence item. Returns null for page-level issues (nothing to highlight).
 */
function buildSelectorForEvidence(
  type: string,
  evidence: IssueEvidenceItem,
): string | null {
  // If the crawler wrote an explicit selector, trust it
  if (typeof evidence.selector === "string" && evidence.selector.trim()) {
    return evidence.selector.trim();
  }

  switch (type) {
    case "broken_internal_link":
    case "broken_external_link":
    case "http_error":
    case "redirect_chain": {
      const href = evidence.brokenHref;
      if (!href) return null;
      const safe = escapeAttr(href);
      // Try the full href first, then common variants: trailing-slash stripped,
      // relative path suffix. The overlay will try each selector in turn.
      const pathOnly = (() => {
        try { return new URL(href, "https://x.invalid").pathname; } catch { return null; }
      })();
      const parts = [`a[href="${safe}"]`, `a[href$="${safe}"]`];
      if (pathOnly && pathOnly !== href) {
        parts.push(`a[href="${escapeAttr(pathOnly)}"]`, `a[href$="${escapeAttr(pathOnly)}"]`);
      }
      // Last-resort: match any <a> whose href contains the trailing
      // filename/slug. Catches cases where the site rewrote the path
      // (e.g. /old-page vs /old-page/ vs /en/old-page) so we still
      // surface the element even under best-effort matching.
      const slug = (pathOnly || href).split("/").filter(Boolean).pop();
      if (slug && slug.length >= 3) {
        parts.push(`a[href*="${escapeAttr(slug)}"]`);
      }
      return parts.join(", ");
    }

    case "broken_fragment": {
      const href = evidence.brokenHref;
      if (!href) return null;
      return `a[href="${escapeAttr(href)}"]`;
    }

    case "missing_alt_text": {
      const src = evidence.imageUrl;
      if (!src) return null;
      const safe = escapeAttr(src);
      // Match by exact src, suffix, or filename contains — covers absolute
      // vs relative vs CDN-rewritten image URLs
      const filename = src.split("/").pop() || src;
      const parts = [
        `img[src="${safe}"]`,
        `img[src$="${safe}"]`,
        `img[src*="${escapeAttr(filename)}"]`,
      ];
      return parts.join(", ");
    }

    case "multiple_h1":
      return "h1";

    case "heading_hierarchy":
      return "h1, h2, h3, h4, h5, h6";

    default:
      return null;
  }
}

/**
 * For issues that don't store per-element evidence (e.g. "images missing
 * alt" might just report a count), return a fallback selector that matches
 * every affected element on the page via live DOM scanning.
 */
function fallbackSelector(type: string): string | null {
  switch (type) {
    case "missing_alt_text":
      return "img:not([alt]), img[alt='']";
    case "multiple_h1":
      return "h1";
    default:
      return null;
  }
}

// ─── Recommendation copy ────────────────────────────────────────────────────

const RECOMMENDATIONS: Record<string, string> = {
  broken_internal_link:       "Update this link to point to the correct page or remove it.",
  broken_external_link:       "Update or remove this external link — the destination is unreachable.",
  broken_fragment:            "This anchor link targets an element ID that doesn't exist on this page. Add the target ID or update the link.",
  http_error:                 "This link returns a server error. Check the target URL and fix the server response.",
  missing_title:              "Add a descriptive <title> tag between 30-60 characters.",
  title_length:               "Rewrite the title tag to fit between 30-60 characters.",
  missing_meta_description:   "Add a unique meta description between 150-160 characters.",
  meta_description_length:    "Rewrite the meta description to fit between 150-160 characters.",
  duplicate_title:            "Give this page a unique title — duplicates dilute ranking signals.",
  duplicate_meta_description: "Rewrite this meta description to be unique for this page.",
  missing_h1:                 "Add an <h1> element to this page for clear topic hierarchy.",
  multiple_h1:                "Consolidate to a single <h1> per page — use <h2>/<h3> for subheadings.",
  heading_hierarchy:          "Fix the heading hierarchy — don't skip levels (e.g. h1 → h3 without h2).",
  missing_canonical:          "Add a <link rel=\"canonical\"> tag to this page.",
  missing_alt_text:           "Add descriptive alt text for accessibility and image SEO.",
  missing_landmark:           "Add semantic landmarks like <main>, <nav>, <header>, <footer>.",
  redirect_chain:             "Update the link to point directly to the final destination.",
  orphan_page:                "This page isn't linked from anywhere on your site. Add internal links or remove the page.",
  slow_resource:              "Optimise this resource — it's slowing down page load.",
  other:                      "Review this issue and apply the suggested fix.",
};

const TYPE_BUILDS_MESSAGE: Partial<Record<string, (e: IssueEvidenceItem) => string>> = {
  broken_internal_link:  e => e.brokenHref ? `Link points to ${e.brokenHref} (${e.statusCode ?? "404"})` : "Broken internal link",
  broken_external_link:  e => e.brokenHref ? `External link to ${e.brokenHref} returns ${e.statusCode ?? "error"}` : "Broken external link",
  broken_fragment:       e => e.brokenHref ? `Anchor link ${e.brokenHref} targets a missing element ID` : "Broken anchor link",
  http_error:            e => e.brokenHref ? `${e.brokenHref} returns ${e.statusCode ?? "HTTP error"}` : "HTTP error",
  missing_alt_text:      e => e.imageUrl ? `Image ${e.imageUrl} is missing an alt attribute` : "Image missing alt text",
  redirect_chain:        e => {
    const chain = Array.isArray(e.redirectChain) ? e.redirectChain.join(" → ") : null;
    return chain ? `Redirect chain: ${chain}` : (e.brokenHref ? `${e.brokenHref} goes through multiple redirects` : "Redirect chain");
  },
};

// ─── Public mapper ──────────────────────────────────────────────────────────

export type IssueInput = {
  id: string;
  title: string;
  category: string;
  severity: string;
  detail: string | null;
  pageUrl: string | null;
  evidence: Prisma.JsonValue;
};

/**
 * Convert a TechnicalIssue row into one OR MORE OverlayIssue entries.
 *
 * - Element-level issues: one OverlayIssue per evidence item that belongs
 *   to `pageUrl` (filtered by normalised sourcePageUrl).
 * - Page-level issues: a single OverlayIssue with selector=null.
 *
 * Each returned OverlayIssue has a stable synthetic id of the form
 * `<issueId>:<evidenceIndex>` so the overlay can deep-link to a specific
 * broken element (not just the aggregate row).
 */
export function mapIssueForOverlay(
  issue: IssueInput,
  pageUrl: string | null,
): OverlayIssue[] {
  const type = normaliseType(issue.title);
  const severity = normaliseSeverity(issue.severity);
  const recommendation = RECOMMENDATIONS[type] ?? RECOMMENDATIONS.other;

  const evidenceList: IssueEvidenceItem[] = Array.isArray(issue.evidence)
    ? (issue.evidence as unknown as IssueEvidenceItem[])
    : [];

  // ── Element-level issues: explode every evidence item ──────────────────
  const isElementLevel =
    type === "broken_internal_link" ||
    type === "broken_external_link" ||
    type === "broken_fragment" ||
    type === "http_error" ||
    type === "redirect_chain" ||
    type === "missing_alt_text";

  if (isElementLevel && evidenceList.length > 0) {
    const targetPath = pageUrl ? normalisePath(pageUrl) : null;
    const out: OverlayIssue[] = [];

    evidenceList.forEach((evidence, idx) => {
      // Filter: only emit evidence items that belong to the current page
      if (targetPath && typeof evidence.sourcePageUrl === "string") {
        if (normalisePath(evidence.sourcePageUrl) !== targetPath) return;
      }

      const selector = buildSelectorForEvidence(type, evidence) ?? fallbackSelector(type);
      if (!selector) return;

      const messageBuilder = TYPE_BUILDS_MESSAGE[type];
      const message = messageBuilder ? messageBuilder(evidence) : (issue.detail ?? issue.title);

      out.push({
        id: `${issue.id}:${idx}`,
        type,
        severity,
        category: issue.category,
        title: issue.title,
        message,
        recommendation,
        selector,
        context: {
          anchorText: typeof evidence.anchorText === "string" ? evidence.anchorText : undefined,
          targetUrl: typeof evidence.brokenHref === "string" ? evidence.brokenHref : (typeof evidence.imageUrl === "string" ? evidence.imageUrl : undefined),
          statusCode: typeof evidence.statusCode === "number" ? evidence.statusCode : undefined,
          snippet: typeof evidence.snippet === "string" ? evidence.snippet.slice(0, 200) : undefined,
          redirectChain: Array.isArray(evidence.redirectChain) ? evidence.redirectChain.filter((s): s is string => typeof s === "string") : undefined,
        },
      });
    });

    return out;
  }

  // ── Element-level issue with no evidence → try the fallback selector ──
  if (isElementLevel) {
    const selector = fallbackSelector(type);
    if (selector) {
      return [{
        id: issue.id,
        type,
        severity,
        category: issue.category,
        title: issue.title,
        message: issue.detail ?? issue.title,
        recommendation,
        selector,
      }];
    }
  }

  // ── Page-level issue: single entry, no selector ────────────────────────
  return [{
    id: issue.id,
    type,
    severity,
    category: issue.category,
    title: issue.title,
    message: issue.detail ?? issue.title,
    recommendation,
    selector: null,
  }];
}
