"use client";

import { createContext, useContext, useMemo, type ReactNode } from "react";

import type { MessageAttachment } from "@/context/UnifiedChatContext";
import { docIconFor } from "@/lib/doc-attachments";
import type { StreamEvent } from "@/lib/unified-ws";

/**
 * Inline file links. The model refers to a file it generated by writing its
 * exact filename in plain prose (e.g. "…saved as DeepTutor_Introduction.pdf");
 * a remark plugin (``makeFileLinkRemarkPlugin``) turns each occurrence of a
 * known generated filename into a clickable link, and clicking it opens the
 * file in the Viewer — the same action as the card under the message (which
 * still renders). Codex-style: the filename in the text becomes a live link.
 *
 * The plugin emits a normal markdown link with the reserved ``attachment:``
 * scheme; the renderers' ``a`` component intercepts it (``parseAttachmentHref``)
 * and renders a compact ``InlineFileCard``. The href survives the markdown URL
 * sanitizer only because ``attachment`` is allow-listed in
 * ``SAFE_MARKDOWN_PROTOCOL_REGEX`` (lib/markdown-display.ts), and these links
 * are never emitted as navigable anchors, so allowing the scheme is safe.
 */
const ATTACHMENT_HREF_PREFIX = "attachment:";

function decode(raw: string): string {
  try {
    return decodeURIComponent(raw.trim());
  } catch {
    return raw.trim();
  }
}

/** The target filename of an ``attachment:NAME`` href, or null if this href is
 *  not a file reference. */
export function parseAttachmentHref(href?: string): string | null {
  if (!href || !href.startsWith(ATTACHMENT_HREF_PREFIX)) return null;
  const name = decode(href.slice(ATTACHMENT_HREF_PREFIX.length));
  return name || null;
}

// ---------------------------------------------------------------------------
// Generated-file collection (shared by the provider and the message's card
// block). Persisted ``generated`` attachments merged with artifacts streamed
// live in ``tool_result`` events, deduped by URL.
// ---------------------------------------------------------------------------

export function extractStreamedArtifacts(
  events?: StreamEvent[],
): MessageAttachment[] {
  const out: MessageAttachment[] = [];
  for (const ev of events ?? []) {
    if (ev.type !== "tool_result") continue;
    const meta = (ev.metadata ?? {}) as {
      tool_metadata?: {
        artifacts?: Array<{
          filename?: string;
          url?: string;
          mime_type?: string;
          size_bytes?: number;
        }>;
      };
    };
    for (const a of meta.tool_metadata?.artifacts ?? []) {
      if (!a?.url) continue;
      out.push({
        type: a.mime_type?.startsWith("image/") ? "image" : "document",
        filename: a.filename,
        url: a.url,
        mime_type: a.mime_type,
        size_bytes: a.size_bytes,
        generated: true,
      });
    }
  }
  return out;
}

export function mergeGeneratedFiles(
  attachments: MessageAttachment[],
  events?: StreamEvent[],
): MessageAttachment[] {
  const persisted = attachments.filter((a) => a.generated);
  const seen = new Set(persisted.map((a) => a.url).filter(Boolean));
  const merged = [...persisted];
  for (const a of extractStreamedArtifacts(events)) {
    if (a.url && seen.has(a.url)) continue;
    if (a.url) seen.add(a.url);
    merged.push(a);
  }
  return merged;
}

// ---------------------------------------------------------------------------
// remark plugin: linkify exact filename mentions in prose.
// ---------------------------------------------------------------------------

// mdast node types whose text must NOT be linkified (code, existing links,
// math). Filenames inside these are shown verbatim.
const SKIP_NODE_TYPES = new Set([
  "code",
  "inlineCode",
  "link",
  "linkReference",
  "definition",
  "math",
  "inlineMath",
  "html",
]);

const NAME_CHAR = /[A-Za-z0-9]/;

function boundaryOk(text: string, start: number, len: number): boolean {
  const before = text[start - 1];
  const after = text[start + len];
  // Don't match inside a larger token/path: reject an alphanumeric, '.', '/'
  // or '\' immediately before, or an alphanumeric right after the extension.
  if (
    before &&
    (NAME_CHAR.test(before) ||
      before === "." ||
      before === "/" ||
      before === "\\")
  ) {
    return false;
  }
  if (after && NAME_CHAR.test(after)) return false;
  return true;
}

/**
 * Build a remark plugin that wraps every plain-text occurrence of a known
 * generated filename in an ``attachment:`` link. Matches the exact filename
 * and a separator-normalized variant (``_``/``-`` ⇄ space), longest first.
 * Returns null when there are no filenames to match.
 */
export function makeFileLinkRemarkPlugin(files: MessageAttachment[]) {
  const entries: Array<{ needle: string; name: string }> = [];
  const seen = new Set<string>();
  for (const file of files) {
    const name = file.filename;
    if (!name) continue;
    const variants = new Set([name, name.replace(/[_-]+/g, " ")]);
    for (const needle of variants) {
      const key = `${needle}${name}`;
      if (needle && !seen.has(key)) {
        seen.add(key);
        entries.push({ needle, name });
      }
    }
  }
  if (!entries.length) return null;
  // Prefer the most specific (longest) needle when several could match.
  entries.sort((a, b) => b.needle.length - a.needle.length);

  // Surface → real filename, for resolving a link the model wrote itself
  // (e.g. `[Agentic_RAG_Guide.pdf](Agentic_RAG_Guide.pdf)`): match the link's
  // url basename or its label against a known filename.
  const surfaceToName = new Map<string, string>();
  const addSurface = (surface: string, name: string) => {
    for (const key of [surface.trim(), surface.trim().toLowerCase()]) {
      if (key && !surfaceToName.has(key)) surfaceToName.set(key, name);
    }
  };
  const baseName = (s: string) => s.split(/[\\/]/).pop() ?? s;
  for (const file of files) {
    const name = file.filename;
    if (!name) continue;
    addSurface(name, name);
    addSurface(name.replace(/[_-]+/g, " "), name);
    addSurface(baseName(name), name);
  }
  const lookupSurface = (s: string): string | undefined =>
    surfaceToName.get(s) ??
    surfaceToName.get(s.trim()) ??
    surfaceToName.get(s.trim().toLowerCase());

  const linkLabel = (node: Record<string, unknown>): string => {
    const parts: string[] = [];
    const walk = (n: Record<string, unknown> | undefined) => {
      if (!n) return;
      if (n.type === "text" && typeof n.value === "string") parts.push(n.value);
      const kids = n.children as Array<Record<string, unknown>> | undefined;
      if (Array.isArray(kids)) kids.forEach(walk);
    };
    (node.children as Array<Record<string, unknown>> | undefined)?.forEach(
      walk,
    );
    return parts.join("");
  };

  // A model-written link points at one of our files when its url basename or
  // its label is a known filename — return the real filename to link to.
  const linkTargetFile = (
    node: Record<string, unknown>,
  ): string | undefined => {
    const url = typeof node.url === "string" ? node.url : "";
    if (url.startsWith(ATTACHMENT_HREF_PREFIX)) return undefined; // already ours
    return (
      lookupSurface(decode(baseName(url))) ?? lookupSurface(linkLabel(node))
    );
  };

  const splitText = (value: string): Array<Record<string, unknown>> => {
    const nodes: Array<Record<string, unknown>> = [];
    let i = 0;
    while (i < value.length) {
      let hit: { idx: number; needle: string; name: string } | null = null;
      for (const e of entries) {
        const idx = value.indexOf(e.needle, i);
        if (idx === -1 || !boundaryOk(value, idx, e.needle.length)) continue;
        if (
          !hit ||
          idx < hit.idx ||
          (idx === hit.idx && e.needle.length > hit.needle.length)
        ) {
          hit = { idx, needle: e.needle, name: e.name };
        }
      }
      if (!hit) {
        nodes.push({ type: "text", value: value.slice(i) });
        break;
      }
      if (hit.idx > i) {
        nodes.push({ type: "text", value: value.slice(i, hit.idx) });
      }
      nodes.push({
        type: "link",
        url: `${ATTACHMENT_HREF_PREFIX}${encodeURIComponent(hit.name)}`,
        title: null,
        children: [{ type: "text", value: hit.needle }],
      });
      i = hit.idx + hit.needle.length;
    }
    return nodes;
  };

  const visit = (node: Record<string, unknown>): void => {
    const children = node.children as
      | Array<Record<string, unknown>>
      | undefined;
    if (!Array.isArray(children)) return;
    const out: Array<Record<string, unknown>> = [];
    for (const child of children) {
      if (child.type === "text" && typeof child.value === "string") {
        out.push(...splitText(child.value));
      } else if (child.type === "link") {
        // The model often writes the file as a link itself, e.g.
        // `[name.pdf](name.pdf)` — a broken relative href that would navigate.
        // Redirect it to the file instead. Don't recurse (keep its label).
        const real = linkTargetFile(child);
        if (real) {
          child.url = `${ATTACHMENT_HREF_PREFIX}${encodeURIComponent(real)}`;
        }
        out.push(child);
      } else {
        if (!SKIP_NODE_TYPES.has(child.type as string)) visit(child);
        out.push(child);
      }
    }
    node.children = out;
  };

  return () => (tree: Record<string, unknown>) => visit(tree);
}

// ---------------------------------------------------------------------------
// Context + provider + the inline link component.
// ---------------------------------------------------------------------------

interface InlineFileCardValue {
  files: MessageAttachment[];
  resolve: (name: string) => MessageAttachment | undefined;
  onOpen?: (attachment: MessageAttachment) => void;
}

const InlineFileCardContext = createContext<InlineFileCardValue | null>(null);

export function useInlineFileCardContext(): InlineFileCardValue | null {
  return useContext(InlineFileCardContext);
}

const norm = (name: string): string => name.trim().toLowerCase();
const basename = (name: string): string => name.split(/[\\/]/).pop() ?? name;

/**
 * Scoped per assistant message: supplies the renderers' filename-linkify plugin
 * and the inline links with the message's generated files + open callback.
 * Without a provider (book chat, co-writer, …) the plugin sees no files and
 * ``attachment:`` links fall back to plain text, so the generic renderers stay
 * generic.
 */
export function InlineFileCardProvider({
  attachments,
  events,
  onOpen,
  children,
}: {
  attachments: MessageAttachment[];
  events?: StreamEvent[];
  onOpen?: (attachment: MessageAttachment) => void;
  children: ReactNode;
}) {
  const files = useMemo(
    () => mergeGeneratedFiles(attachments, events),
    [attachments, events],
  );
  const value = useMemo<InlineFileCardValue>(() => {
    const exact = new Map<string, MessageAttachment>();
    const lower = new Map<string, MessageAttachment>();
    const base = new Map<string, MessageAttachment>();
    for (const file of files) {
      const name = file.filename;
      if (!name) continue;
      if (!exact.has(name)) exact.set(name, file);
      if (!lower.has(norm(name))) lower.set(norm(name), file);
      if (!base.has(norm(basename(name)))) base.set(norm(basename(name)), file);
    }
    const resolve = (name: string): MessageAttachment | undefined =>
      exact.get(name) ??
      lower.get(norm(name)) ??
      base.get(norm(basename(name)));
    return { files, resolve, onOpen };
  }, [files, onOpen]);
  return (
    <InlineFileCardContext.Provider value={value}>
      {children}
    </InlineFileCardContext.Provider>
  );
}

/**
 * Renders an ``attachment:NAME`` link as a compact, clickable file chip
 * (icon + filename) that opens the file in the Viewer. Falls back to plain text
 * when the name can't be resolved (no provider, or the file no longer exists),
 * so a stray reference is never a broken link.
 */
export function InlineFileCard({
  name,
  fallback,
}: {
  name: string;
  fallback?: ReactNode;
}) {
  const ctx = useContext(InlineFileCardContext);
  const attachment = ctx?.resolve(name);
  if (!attachment) {
    return <>{fallback ?? name}</>;
  }
  const filename = attachment.filename || name;
  const spec = docIconFor(filename);
  const Icon = spec.Icon;
  return (
    <button
      type="button"
      onClick={ctx?.onOpen ? () => ctx.onOpen?.(attachment) : undefined}
      className="inline-flex max-w-full items-center gap-1 align-baseline rounded-md px-1 py-0.5 font-medium text-[var(--primary)] underline decoration-[var(--primary)]/40 underline-offset-2 transition-colors hover:bg-[var(--primary)]/5 hover:decoration-[var(--primary)]"
    >
      <Icon className={`h-[14px] w-[14px] shrink-0 ${spec.tint}`} />
      <span className="truncate">{filename}</span>
    </button>
  );
}
