import { useMemo, useState } from 'react';
import { ExternalLink } from 'lucide-react';
import { Button } from '@/components/ui/button';
import * as st from '@/content/styles';
import { cn } from '@/lib/utils';

export interface CodeReceiptProps {
  /** The file's text, imported with `?raw` so it cannot drift from the source. */
  code: string;
  /** Repo-relative path, shown as the receipt's header. */
  path: string;
  /**
   * A literal string that appears in the source and marks the interesting part.
   * See `resolveMarkedRange` for the two conventions it supports.
   */
  marker?: string;
  /** Link to the whole file — GitHub, usually. */
  href?: string;
  /** Lines of context kept around the marked range while collapsed. */
  context?: number;
  className?: string;
}

export interface MarkedRange {
  /** 0-based, inclusive. */
  start: number;
  /** 0-based, inclusive. */
  end: number;
}

/**
 * True when a line is nothing but the marker inside a comment — `# --8<-- x`,
 * `// region: grader`. Such a line is a POINTER to the code below it, not the
 * code itself, and marking only the comment is the most common way this panel
 * ends up highlighting nothing worth reading.
 */
function isPointerLine(line: string, marker: string): boolean {
  const withoutMarker = line.replace(marker, '');
  return withoutMarker.replace(/[#/*\-<!>\s]/g, '') === '';
}

/**
 * Two marker conventions, because both exist in real repos:
 *
 *   TWICE  — the marker brackets a region (`# --8<-- reward` … `# --8<-- reward`).
 *            The marked range is the lines BETWEEN them; the fences themselves
 *            are not interesting code.
 *
 *   ONCE   — the marker names a construct. If it sits ON the construct
 *            (`def compute_reward`), the range is that line plus its indented
 *            body. If it sits in a comment ABOVE it, the range starts at the
 *            next real line instead — a fence comment is a pointer, and
 *            highlighting the pointer rather than the function is a silent,
 *            plausible-looking failure.
 *
 * Blank lines inside a body are kept; trailing blank ones are not, or the
 * highlight runs on past the end of the function.
 *
 * A marker that matches nothing returns null and the whole file renders. That
 * is the right failure: a stale marker must not hide the source.
 */
export function resolveMarkedRange(lines: string[], marker?: string): MarkedRange | null {
  if (!marker) return null;
  const hits: number[] = [];
  lines.forEach((line, index) => {
    if (line.includes(marker)) hits.push(index);
  });

  const first = hits[0];
  if (first === undefined) return null;

  if (hits.length >= 2) {
    const last = hits[hits.length - 1] as number;
    return last - first > 1 ? { start: first + 1, end: last - 1 } : { start: first, end: last };
  }

  let start = first;
  if (isPointerLine(lines[first] ?? '', marker)) {
    const next = lines.findIndex((line, index) => index > first && line.trim() !== '');
    if (next === -1) return { start: first, end: first };
    start = next;
  }

  const anchor = lines[start] ?? '';
  const indent = anchor.length - anchor.trimStart().length;
  let end = start;
  for (let i = start + 1; i < lines.length; i += 1) {
    const line = lines[i] ?? '';
    if (line.trim() === '') continue;
    const lineIndent = line.length - line.trimStart().length;
    if (lineIndent <= indent) break;
    end = i;
  }
  return { start, end };
}

/**
 * Source, with the part that matters marked.
 *
 * Syntax highlighting is deliberately absent: it costs a highlighter in the
 * bundle and buys nothing an exec can use, while the marked range — the thing
 * that says "this, right here, is the grader" — costs nothing and is the whole
 * reason the panel exists.
 *
 * The block scrolls horizontally inside itself. Long Python lines must never
 * make the PAGE scroll sideways; on a phone that turns every vertical swipe
 * into a fight.
 */
export function CodeReceipt({
  code,
  path,
  marker,
  href,
  context = 3,
  className,
}: CodeReceiptProps) {
  const lines = useMemo(() => code.replace(/\n$/, '').split('\n'), [code]);
  const range = useMemo(() => resolveMarkedRange(lines, marker), [lines, marker]);
  const [expanded, setExpanded] = useState(range === null);

  const shown = useMemo(() => {
    if (range === null || expanded) return { from: 0, to: lines.length - 1 };
    return {
      from: Math.max(range.start - context, 0),
      to: Math.min(range.end + context, lines.length - 1),
    };
  }, [range, expanded, context, lines.length]);

  const visible = lines.slice(shown.from, shown.to + 1);
  const gutterWidth = `${String(lines.length).length + 1}ch`;

  return (
    <section aria-label={`Source: ${path}`} className={cn('card overflow-hidden', className)}>
      <header className="flex flex-col items-start gap-2 border-b border-border px-3 py-2 sm:flex-row sm:items-center sm:gap-x-3">
        {/* Full width on a phone: sharing a row with two controls truncates a
            path to "envs/wo…", which names nothing. */}
        <h3
          className="nums w-full min-w-0 truncate font-mono text-xs text-muted sm:flex-1"
          title={path}
        >
          {path}
        </h3>
        <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
          {range && !expanded ? (
            <Button variant="outline" size="sm" className="tap" onClick={() => setExpanded(true)}>
              Show all {lines.length} lines
            </Button>
          ) : null}
          {range && expanded ? (
            <Button variant="outline" size="sm" className="tap" onClick={() => setExpanded(false)}>
              Collapse to the marked part
            </Button>
          ) : null}
          {href ? (
            <a
              href={href}
              rel="noreferrer"
              className={cn(st.link, 'tap inline-flex items-center gap-1 text-xs')}
            >
              Read the whole file
              <ExternalLink className="size-3" aria-hidden="true" />
            </a>
          ) : null}
        </div>
      </header>

      <div className="max-h-96 overflow-auto">
        <pre className="w-max min-w-full py-2 font-mono text-xs leading-relaxed">
          <code>
            {shown.from > 0 ? (
              <span className="block px-3 text-muted" aria-hidden="true">
                …
              </span>
            ) : null}
            {visible.map((line, offset) => {
              const number = shown.from + offset;
              const marked = range !== null && number >= range.start && number <= range.end;
              return (
                <span
                  key={number}
                  className={cn(
                    'block border-l-2 pr-4',
                    marked
                      ? 'border-brand bg-accent-subtle/60 text-fg'
                      : 'border-transparent text-muted',
                  )}
                >
                  {/* Sticky so the line numbers survive a horizontal scroll —
                      without it they slide out of view exactly when a long
                      line makes you want them. */}
                  <span
                    aria-hidden="true"
                    className="sticky left-0 inline-block select-none bg-surface pr-3 text-right text-muted"
                    style={{ width: gutterWidth }}
                  >
                    {number + 1}
                  </span>
                  {line === '' ? ' ' : line}
                </span>
              );
            })}
            {shown.to < lines.length - 1 ? (
              <span className="block px-3 text-muted" aria-hidden="true">
                …
              </span>
            ) : null}
          </code>
        </pre>
      </div>
    </section>
  );
}
