import type { CSSProperties, ReactNode } from 'react';
import { useEffect, useState } from 'react';
import { Copy, Check } from 'lucide-react';
import { highlight } from '../../FileViewer/renderers/highlight';
import { copyToClipboard } from 'helpers/clipboard';

/**
 * The one way this app draws a block of code: shiki-highlighted when a language is known, line-numbered,
 * horizontally scrolled rather than wrapped, with copy-as-code and copy-as-markdown always visible.
 *
 * It exists as its own component because a chat reply and a tool call show the same thing — a slab of
 * monospace you want to read and then take somewhere — and they had drifted into two different surfaces,
 * one of them green-on-black. Anything that renders code should reach for this rather than a bare `<pre>`.
 */

/**
 * Re-fence the code so it survives being pasted somewhere that renders markdown — a chat to a colleague,
 * an issue, a PR description. The fence has to be longer than the longest backtick run *inside* the code,
 * or a snippet that itself contains a fence closes the block early and the rest leaks out as prose.
 */
function asMarkdown(code: string, lang: string): string {
  const longestRun = Math.max(0, ...[...code.matchAll(/`+/g)].map((m) => m[0].length));
  const fence = '`'.repeat(Math.max(3, longestRun + 1));
  return `${fence}${lang}\n${code}\n${fence}`;
}

// Long enough to sit out the gap between streamed chunks, short enough to feel immediate once a message
// lands. Highlighting every chunk would re-tokenise the whole block each time — quadratic in its length,
// for colours nobody reads on text that is still arriving.
const SETTLE_MS = 120;

// Colours are hardcoded light-on-dark because the block behind these sits at #0d1117 in both themes.
// The blur is not decoration: once the controls became sticky they sit over moving text for the whole
// height of a long block, and a 10%-white pill with code sliding under it is unreadable.
const CONTROL =
  'flex h-[22px] min-w-[22px] cursor-pointer items-center justify-center rounded border border-white/15 ' +
  'bg-white/10 px-1 text-white/70 backdrop-blur-sm transition-colors hover:border-white/30 hover:bg-white/20 ' +
  'hover:text-white';

type CopyMode = 'raw' | 'md';

type CodeSurfaceProps = {
  code: string;
  lang?: string;
  /** The real number of the first line. `Read` reports a slice of a file, not the whole of it. */
  startLine?: number;
  /** Collapse to this many lines behind a show-more. Copy still yields the whole thing. */
  maxLines?: number;
  /** Off for content that only looks like code — a `key: value` dump has no lines worth citing. */
  numbered?: boolean;
  /** Shown when there is no code at all, for a `<pre>` that arrived without a `<code>` inside it. */
  fallback?: ReactNode;
};

export const CodeSurface = ({ code, lang = '', startLine = 1, maxLines, numbered, fallback }: CodeSurfaceProps) => {
  const [copied, setCopied] = useState<CopyMode | null>(null);
  const [expanded, setExpanded] = useState(false);
  // Keyed by the source it was produced from. While a block is still streaming the stored html belongs to
  // a shorter version of the code, and showing it would make the block look like it had stopped growing.
  const [rendered, setRendered] = useState<{ code: string; html: string } | null>(null);

  const allLines = code ? code.split('\n') : [];
  const hidden = maxLines ? allLines.length - maxLines : 0;
  const shown = hidden > 0 && !expanded ? allLines.slice(0, maxLines) : allLines;
  const visible = shown.join('\n');

  useEffect(() => {
    if (!lang || !visible) return;
    let cancelled = false;
    const timer = setTimeout(() => {
      highlight(visible, lang).then((html) => {
        // A fence naming something shiki has no grammar for resolves null; the plain block is the answer.
        if (!cancelled && html) setRendered({ code: visible, html });
      });
    }, SETTLE_MS);
    return () => {
      cancelled = true;
      clearTimeout(timer);
    };
  }, [visible, lang]);

  const html = rendered?.code === visible ? rendered.html : null;

  // A lone "1" beside a one-line block is decoration. Numbers earn their gutter once there is something
  // to count, which is also exactly when you want to say "line 12" out loud.
  const showNumbers = numbered ?? allLines.length > 1;
  const digits = Math.max(2, String(startLine + allLines.length).length);

  const handleCopy = async (mode: CopyMode) => {
    // Always the whole block, never the truncated view — a copy that silently stops at line 20 is worse
    // than no copy at all. The trailing newline goes: pasting it into a shell runs the command.
    const text = code.replace(/\n+$/, '');
    if (!text) return;
    try {
      await copyToClipboard(mode === 'md' ? asMarkdown(text, lang) : text);
      setCopied(mode);
      setTimeout(() => setCopied(null), 2000);
    } catch {
      /* no clipboard permission — the text is still selectable */
    }
  };

  return (
    <div
      className="code-surface"
      data-lang={lang || undefined}
      data-numbered={showNumbers || undefined}
      style={{ '--line-digits': digits, '--line-offset': startLine - 1 } as CSSProperties}
    >
      {code && (
        // Sticky, not absolute, and first in the flow so it has a flow position to stick to. A block
        // taller than its scroll container — a live log tail, a 400-line read — used to carry its
        // controls off the top edge, so the output you most want to copy was the one whose copy button
        // you had to scroll back up to find. `h-0` keeps the row out of the vertical flow: it overlays
        // the top of the block rather than pushing it down, which is what the old `absolute` did.
        //
        // Always visible, not hover-revealed: a control you have to discover by waving the pointer at it
        // is a control most people never find, and on touch there is no hover to find it with at all. Two
        // buttons rather than one with a modifier — the destination decides which you want (a terminal, or
        // someone else's markdown), and a modifier nobody can see is a feature nobody uses.
        <div className="sticky top-1.5 z-10 flex h-0 items-start justify-end gap-1.5 pr-1.5">
          {lang && (
            <span className="rounded px-1 font-mono text-[10px] tracking-wide text-white/35 backdrop-blur-sm select-none">
              {lang}
            </span>
          )}
          <button
            type="button"
            onClick={() => handleCopy('raw')}
            title="Copy the code"
            aria-label="Copy the code"
            className={CONTROL}
          >
            {copied === 'raw' ? <Check className="h-3.5 w-3.5 text-green-400" /> : <Copy className="h-3.5 w-3.5" />}
          </button>
          <button
            type="button"
            onClick={() => handleCopy('md')}
            title="Copy as a markdown block"
            aria-label="Copy as a markdown block"
            className={CONTROL}
          >
            {copied === 'md' ? (
              <Check className="h-3.5 w-3.5 text-green-400" />
            ) : (
              // The label is what it produces. Backtick glyphs sit near cap height, so the nudge is what
              // optically centres them in the box — flex centring alone leaves them riding high.
              <span className="translate-y-[2px] font-mono text-[11px] leading-none">```</span>
            )}
          </button>
        </div>
      )}

      {html ? (
        <div dangerouslySetInnerHTML={{ __html: html }} />
      ) : (
        // The plain branch mimics shiki's per-line spans so a block is numbered before it is coloured, and
        // nothing shifts sideways when the highlight lands.
        <pre>
          {code ? (
            <code>
              {shown.map((line, i) => (
                <span className="line" key={i}>
                  {i < shown.length - 1 ? `${line}\n` : line}
                </span>
              ))}
            </code>
          ) : (
            fallback
          )}
        </pre>
      )}

      {hidden > 0 && (
        <button
          onClick={() => setExpanded(!expanded)}
          className="mt-1 cursor-pointer text-xs text-duck-teal hover:underline"
        >
          {expanded ? 'Show less' : `Show more (${hidden} more lines)`}
        </button>
      )}
    </div>
  );
};
