import Link from "next/link";
import { Fragment } from "react";
import { ALL_ORGANIZATIONS } from "@/lib/sag/entities";

/**
 * Wiki-style entity backlink renderer (PUNCH_LIST D3).
 *
 * Scans a document body for entity name occurrences (from
 * `ALL_ORGANIZATIONS`) and Obsidian `[[wiki-link]]` patterns, then renders
 * the text with matched mentions wrapped in `<Link>` to
 * `/app/entities/[slug]`.
 *
 * Inputs come from arbitrary user-supplied prose (extracted OCR text,
 * decoded inline markdown, etc.), so the matcher is intentionally
 * forgiving: case-insensitive, common legal suffixes stripped, longest-
 * match preference.
 */

export type EntityMention = {
  start: number;
  end: number;
  /** The exact text as it appears in the body (preserves casing). */
  matchedText: string;
  /** Resolved entity slug, or `null` for unresolved `[[wiki-links]]`. */
  entitySlug: string | null;
  source: "name" | "dba" | "wiki-link";
};

/**
 * Legal/business suffixes stripped when building lookup keys so that
 * "Carolina Hemp Tours, LLC" and "Carolina Hemp Tours" both match.
 * Order matters — longer/more specific suffixes come first.
 */
const SUFFIX_PATTERNS: RegExp[] = [
  /,?\s+l\.?l\.?c\.?$/i,
  /,?\s+inc\.?$/i,
  /,?\s+corp\.?$/i,
  /,?\s+co\.?$/i,
  /,?\s+l\.?p\.?$/i,
  /,?\s+ltd\.?$/i,
  /,?\s+company$/i,
];

function stripSuffix(raw: string): string {
  let out = raw.trim();
  // Apply repeatedly in case of doubled suffixes like "Foo Inc, LLC".
  let changed = true;
  while (changed) {
    changed = false;
    for (const pattern of SUFFIX_PATTERNS) {
      const next = out.replace(pattern, "");
      if (next !== out) {
        out = next.trim();
        changed = true;
      }
    }
  }
  return out;
}

function normalizeKey(raw: string): string {
  // Collapse whitespace, lowercase, then strip trailing legal suffixes.
  const collapsed = raw.replace(/\s+/g, " ").trim().toLowerCase();
  return stripSuffix(collapsed);
}

type LookupEntry = {
  key: string; // normalized
  slug: string;
  source: "name" | "dba";
};

/**
 * Cached lookup index built once at module load. Re-uses the lazy
 * pattern in case `ALL_ORGANIZATIONS` is mutated in tests.
 */
let cachedLookup: LookupEntry[] | null = null;

function getLookup(): LookupEntry[] {
  if (cachedLookup) return cachedLookup;
  const seen = new Map<string, LookupEntry>();
  for (const org of ALL_ORGANIZATIONS) {
    const candidates: Array<{ raw: string; source: "name" | "dba" }> = [
      { raw: org.name, source: "name" },
    ];
    if (org.dba) candidates.push({ raw: org.dba, source: "dba" });
    for (const c of candidates) {
      const key = normalizeKey(c.raw);
      if (!key) continue;
      // Skip extremely short keys to avoid runaway matches like "Co".
      if (key.length < 3) continue;
      // First write wins for a given key — names beat DBAs because they
      // come first in the candidate list.
      if (!seen.has(key)) {
        seen.set(key, { key, slug: org.slug, source: c.source });
      }
    }
  }
  cachedLookup = Array.from(seen.values()).sort(
    // Longest key first so the scan prefers the most-specific match.
    (a, b) => b.key.length - a.key.length
  );
  return cachedLookup;
}

/** Exported for tests; resets the memoized lookup. */
export function __resetEntityLookupCache(): void {
  cachedLookup = null;
}

/** Count of distinct lookup keys built from name + dba aliases. */
export function entityLookupSize(): number {
  return getLookup().length;
}

const WIKI_LINK_PATTERN = /\[\[([^\[\]\n]+?)\]\]/g;

function isWordBoundary(char: string | undefined): boolean {
  if (char === undefined) return true;
  // A word character in our sense is a letter, digit, or underscore.
  // Apostrophes and hyphens inside names are handled by the lookup
  // already (they're part of the key), so treating them as boundaries
  // here is fine — we only check the chars *outside* the match.
  return !/[a-zA-Z0-9_]/.test(char);
}

/**
 * Find every entity mention and `[[wiki-link]]` in `text`. Overlapping
 * matches are resolved by preferring the longer match; ties prefer the
 * earlier start. The returned array is sorted by `start` ascending.
 */
export function findEntityMentions(text: string): EntityMention[] {
  if (!text) return [];
  const lookup = getLookup();
  const lower = text.toLowerCase();
  const raw: EntityMention[] = [];

  // 1) Wiki-link scan first — these win over plain-name matches inside
  //    the brackets because they are explicit user intent.
  WIKI_LINK_PATTERN.lastIndex = 0;
  let wm: RegExpExecArray | null;
  while ((wm = WIKI_LINK_PATTERN.exec(text)) !== null) {
    const inner = wm[1];
    const key = normalizeKey(inner);
    const hit = lookup.find((entry) => entry.key === key);
    raw.push({
      start: wm.index,
      end: wm.index + wm[0].length,
      matchedText: wm[0],
      entitySlug: hit ? hit.slug : null,
      source: "wiki-link",
    });
  }

  // 2) Plain-name scan. We walk the lookup (longest-first) and use
  //    `indexOf` on the lowercased text. Word boundaries are enforced
  //    by inspecting the neighboring chars in the original text.
  for (const entry of lookup) {
    let from = 0;
    while (from <= lower.length) {
      const idx = lower.indexOf(entry.key, from);
      if (idx < 0) break;
      const end = idx + entry.key.length;
      const before = idx > 0 ? text[idx - 1] : undefined;
      const after = end < text.length ? text[end] : undefined;
      if (isWordBoundary(before) && isWordBoundary(after)) {
        raw.push({
          start: idx,
          end,
          matchedText: text.slice(idx, end),
          entitySlug: entry.slug,
          source: entry.source,
        });
      }
      from = idx + entry.key.length;
    }
  }

  if (raw.length === 0) return [];

  // 3) Resolve overlaps. Sort by start ascending, then by length
  //    descending so the longer match comes first when starts tie.
  raw.sort((a, b) => {
    if (a.start !== b.start) return a.start - b.start;
    return b.end - b.start - (a.end - a.start);
  });

  const kept: EntityMention[] = [];
  for (const m of raw) {
    const last = kept[kept.length - 1];
    if (!last) {
      kept.push(m);
      continue;
    }
    const overlaps = m.start < last.end;
    if (!overlaps) {
      kept.push(m);
      continue;
    }
    // Overlap — keep whichever is longer. Wiki-links are kept whenever
    // they overlap with a plain-name match because they encode explicit
    // user intent (the brackets).
    const lastLen = last.end - last.start;
    const curLen = m.end - m.start;
    if (m.source === "wiki-link" && last.source !== "wiki-link") {
      kept[kept.length - 1] = m;
    } else if (last.source === "wiki-link" && m.source !== "wiki-link") {
      // keep last
    } else if (curLen > lastLen) {
      kept[kept.length - 1] = m;
    }
    // otherwise keep last
  }

  return kept;
}

function renderTextWithLineBreaks(text: string, keyPrefix: string): React.ReactNode {
  if (!text.includes("\n")) return text;
  const parts = text.split("\n");
  return parts.map((part, i) => (
    <Fragment key={`${keyPrefix}-${i}`}>
      {part}
      {i < parts.length - 1 && <br />}
    </Fragment>
  ));
}

/**
 * Renders `text` with entity mentions auto-linked to
 * `/app/entities/[slug]`. Newlines are preserved as `<br />` so the
 * caller can keep `whitespace-pre-wrap` for indentation while still
 * letting matched links wrap.
 */
export function EntityLinkedText({ text }: { text: string }): React.ReactElement {
  const mentions = findEntityMentions(text);
  if (mentions.length === 0) {
    return <>{renderTextWithLineBreaks(text, "plain")}</>;
  }

  const nodes: React.ReactNode[] = [];
  let cursor = 0;
  mentions.forEach((m, idx) => {
    if (m.start > cursor) {
      const chunk = text.slice(cursor, m.start);
      nodes.push(
        <Fragment key={`t-${idx}`}>
          {renderTextWithLineBreaks(chunk, `t-${idx}`)}
        </Fragment>
      );
    }
    if (m.entitySlug) {
      nodes.push(
        <Link
          key={`l-${idx}`}
          href={`/app/entities/${m.entitySlug}`}
          className="underline decoration-dotted decoration-muted-foreground/40 underline-offset-2 hover:decoration-foreground transition-colors"
        >
          {m.matchedText}
        </Link>
      );
    } else {
      // Unresolved [[wiki-link]] — keep the brackets visible so the user
      // can see they referenced something we don&apos;t recognize.
      nodes.push(
        <span
          key={`u-${idx}`}
          className="text-muted-foreground italic"
          title="No matching entity"
        >
          {m.matchedText}
        </span>
      );
    }
    cursor = m.end;
  });
  if (cursor < text.length) {
    const tail = text.slice(cursor);
    nodes.push(
      <Fragment key="tail">{renderTextWithLineBreaks(tail, "tail")}</Fragment>
    );
  }

  return <>{nodes}</>;
}
