import { Children, cloneElement, isValidElement, useState } from 'react';
import type { ReactElement, ReactNode } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { ChevronDown, ChevronRight, Copy, RefreshCw, Check, Share2 } from 'lucide-react';
import { toast } from 'sonner';
import type { Chat, Message } from '@/api/types';
import { api } from '@/api/client';
import { sessionEvents } from '@/hooks/sessionEvents';
import { ToolCallCard } from './ToolCallCard';
import { CodeBlock } from './CodeBlock';

// Match path-shaped substrings ending in `.ext`. Additionally require a `/`
// in the match to reduce false positives in prose (e.g. plain `foo.ts` won't
// match, but `src/foo.ts` will). False positives at the edges are accepted
// per Sam's design decision (2026-05-14).
const PATH_REGEX = /([a-zA-Z0-9._/-]+\.[a-zA-Z0-9]+)/g;

function isPathLike(s: string): boolean {
  return s.includes('/');
}

function emitOpenFile(path: string): void {
  sessionEvents.emit({ type: 'open_file_in_browser', path });
}

// Split a plain string into a flat array of strings and clickable button
// nodes for path-shaped substrings. If no matches, returns the original
// string verbatim (no array wrapping).
function linkifyPaths(text: string, keyPrefix: string): ReactNode {
  const out: ReactNode[] = [];
  let lastIdx = 0;
  let idx = 0;
  for (const match of text.matchAll(PATH_REGEX)) {
    const matchedText = match[0];
    const start = match.index ?? 0;
    if (!isPathLike(matchedText)) continue;
    if (start > lastIdx) out.push(text.slice(lastIdx, start));
    out.push(
      <button
        key={`${keyPrefix}-${idx}`}
        type="button"
        onClick={() => emitOpenFile(matchedText)}
        className="text-primary underline cursor-pointer hover:text-primary/80"
      >
        {matchedText}
      </button>
    );
    lastIdx = start + matchedText.length;
    idx += 1;
  }
  if (out.length === 0) return text;
  if (lastIdx < text.length) out.push(text.slice(lastIdx));
  return out;
}

// Walk react-markdown children, linkifying string text nodes. Children of
// <code> nodes (CodeBlock and inline code) are left untouched — the regex
// shouldn't run inside code spans.
function linkifyChildren(children: ReactNode, keyPrefix = 'l'): ReactNode {
  const arr = Children.toArray(children);
  return arr.map((child, i) => {
    if (typeof child === 'string') {
      return (
        <span key={`${keyPrefix}-${i}`}>
          {linkifyPaths(child, `${keyPrefix}-${i}`)}
        </span>
      );
    }
    if (isValidElement(child)) {
      const el = child as ReactElement<{ children?: ReactNode }>;
      // Skip inline/block code — paths in code spans aren't link targets.
      if (el.type === 'code' || el.type === CodeBlock) return child;
      const grandchildren = el.props.children;
      if (grandchildren === undefined) return child;
      return cloneElement(el, {
        key: el.key ?? `linkified-${i}`,
        children: linkifyChildren(grandchildren, `${keyPrefix}-${i}`),
      });
    }
    return child;
  });
}

interface Props {
  message: Message;
  sessionChats?: Chat[];
}

function MarkdownBody({ content }: { content: string }) {
  return (
    <Markdown
      remarkPlugins={[remarkGfm]}
      components={{
        pre: ({ children }) => <>{children}</>,
        code: (props) => {
          const { children, className, ...rest } = props as {
            children?: unknown;
            className?: string;
          };
          const text = String(children ?? '').replace(/\n$/, '');
          const langMatch = /language-([\w-]+)/.exec(className ?? '');
          const isBlock = !!langMatch || text.includes('\n');
          if (isBlock) {
            return <CodeBlock code={text} lang={langMatch?.[1]} />;
          }
          return (
            <code
              {...rest}
              className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]"
            >
              {children as React.ReactNode}
            </code>
          );
        },
        a: ({ children, href }) => (
          <a
            href={href}
            target="_blank"
            rel="noreferrer"
            className="underline decoration-muted-foreground/40 underline-offset-2 hover:decoration-foreground"
          >
            {children}
          </a>
        ),
        ul: ({ children }) => (
          <ul className="list-disc pl-5 space-y-1">{children}</ul>
        ),
        ol: ({ children }) => (
          <ol className="list-decimal pl-5 space-y-1">{children}</ol>
        ),
        li: ({ children }) => <li>{linkifyChildren(children)}</li>,
        p: ({ children }) => (
          <p className="leading-relaxed">{linkifyChildren(children)}</p>
        ),
        h1: ({ children }) => <h1 className="text-base font-semibold mt-2">{children}</h1>,
        h2: ({ children }) => <h2 className="text-sm font-semibold mt-2">{children}</h2>,
        h3: ({ children }) => <h3 className="text-sm font-semibold mt-1">{children}</h3>,
        blockquote: ({ children }) => (
          <blockquote className="border-l-2 border-border pl-3 text-muted-foreground">
            {children}
          </blockquote>
        ),
        table: ({ children }) => (
          <div className="overflow-x-auto">
            <table className="border-collapse text-xs">{children}</table>
          </div>
        ),
        th: ({ children }) => (
          <th className="border border-border px-2 py-1 text-left font-medium">{children}</th>
        ),
        td: ({ children }) => (
          <td className="border border-border px-2 py-1">
            {linkifyChildren(children)}
          </td>
        ),
      }}
    >
      {content}
    </Markdown>
  );
}

function StatsLine({ message }: { message: Message }) {
  const tokens = message.tokens_used;
  if (typeof tokens !== 'number' || tokens <= 0) return null;
  const started = message.started_at ? Date.parse(message.started_at) : NaN;
  const finished = message.finished_at ? Date.parse(message.finished_at) : NaN;
  let tps: number | null = null;
  if (!Number.isNaN(started) && !Number.isNaN(finished) && finished > started) {
    const seconds = (finished - started) / 1000;
    if (seconds > 0) tps = Math.round((tokens / seconds) * 10) / 10;
  }
  const ctxUsed = message.ctx_used;
  const ctxMax = message.ctx_max;
  const ctxPart =
    typeof ctxUsed === 'number'
      ? typeof ctxMax === 'number' && ctxMax > 0
        ? `${ctxUsed} / ${ctxMax} ctx`
        : `${ctxUsed} ctx`
      : null;

  const parts: string[] = [`${tokens} tokens`];
  if (tps !== null) parts.push(`${tps.toFixed(1)} tok/s`);
  if (ctxPart) parts.push(ctxPart);

  return (
    <div className="text-[10px] font-mono text-muted-foreground">
      {parts.join(' · ')}
    </div>
  );
}

function ActionRow({
  message,
}: {
  message: Message;
}) {
  const [justCopied, setJustCopied] = useState(false);
  const [regenerating, setRegenerating] = useState(false);

  async function copy() {
    try {
      await navigator.clipboard.writeText(message.content);
      setJustCopied(true);
      setTimeout(() => setJustCopied(false), 1200);
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'copy failed');
    }
  }

  async function regenerate() {
    if (regenerating || message.status === 'streaming') return;
    setRegenerating(true);
    try {
      await api.messages.regenerate(message.chat_id, message.id);
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'regenerate failed');
    } finally {
      setRegenerating(false);
    }
  }

  const isAssistant = message.role === 'assistant';
  const canRegen = isAssistant && message.status !== 'streaming';

  return (
    <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
      <button
        type="button"
        onClick={() => void copy()}
        className="inline-flex items-center justify-center size-6 rounded text-muted-foreground hover:bg-muted hover:text-foreground"
        aria-label="Copy message"
        title="Copy"
      >
        {justCopied ? <Check className="size-3" /> : <Copy className="size-3" />}
      </button>
      {isAssistant && (
        <button
          type="button"
          onClick={() => void regenerate()}
          disabled={!canRegen || regenerating}
          className="inline-flex items-center justify-center size-6 rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed"
          aria-label="Regenerate message"
          title="Regenerate"
        >
          <RefreshCw className={`size-3 ${regenerating ? 'animate-spin' : ''}`} />
        </button>
      )}
    </div>
  );
}

function CompactCard({ message, sessionChats }: { message: Message; sessionChats?: Chat[] }) {
  const [expanded, setExpanded] = useState(false);
  const [copied, setCopied] = useState(false);
  const [shareOpen, setShareOpen] = useState(false);

  const headerMatch = message.content.match(/^\[Context compacted — (\d+) messages summarized\]/);
  const headerText = headerMatch ? headerMatch[0] : 'Context compacted';
  const summaryText = headerMatch
    ? message.content.slice(headerMatch[0].length).trim()
    : message.content;

  async function handleCopy() {
    try {
      await navigator.clipboard.writeText(summaryText);
      setCopied(true);
      setTimeout(() => setCopied(false), 1200);
    } catch {
      toast.error('Copy failed');
    }
  }

  async function handleShareToChat(chatId: string) {
    try {
      await api.messages.send(chatId, summaryText);
      toast.success('Summary sent to chat');
      setShareOpen(false);
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'Failed to share');
    }
  }

  const otherChats = (sessionChats ?? []).filter(
    (c) => c.id !== message.chat_id && c.status === 'open'
  );

  return (
    <div className="rounded-lg border bg-muted/30 text-sm">
      <div className="flex items-center gap-2 px-3 py-2">
        <button
          type="button"
          onClick={() => setExpanded(!expanded)}
          className="flex items-center gap-1.5 flex-1 min-w-0 text-left text-muted-foreground hover:text-foreground"
        >
          {expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
          <span className="text-xs font-medium truncate">{headerText}</span>
        </button>
        <button
          type="button"
          onClick={() => void handleCopy()}
          className="p-1 rounded hover:bg-muted text-muted-foreground"
          aria-label="Copy summary"
        >
          {copied ? <Check size={12} /> : <Copy size={12} />}
        </button>
        {otherChats.length > 0 && (
          <div className="relative">
            <button
              type="button"
              onClick={() => setShareOpen(!shareOpen)}
              className="p-1 rounded hover:bg-muted text-muted-foreground"
              aria-label="Send to chat"
            >
              <Share2 size={12} />
            </button>
            {shareOpen && (
              <div className="absolute right-0 top-full mt-1 z-50 bg-popover border rounded-md shadow-md min-w-[160px] py-1">
                {otherChats.map((c) => (
                  <button
                    key={c.id}
                    type="button"
                    onClick={() => void handleShareToChat(c.id)}
                    className="w-full text-left px-3 py-1.5 text-xs hover:bg-accent truncate"
                  >
                    {c.name ?? 'New chat'}
                  </button>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
      {expanded && (
        <div className="px-3 pb-3 text-xs leading-relaxed text-muted-foreground whitespace-pre-wrap border-t pt-2">
          {summaryText}
        </div>
      )}
    </div>
  );
}

export function MessageBubble({ message, sessionChats }: Props) {
  if (message.kind === 'compact') {
    return <CompactCard message={message} sessionChats={sessionChats} />;
  }

  if (message.role === 'tool') {
    return <ToolCallCard message={message} />;
  }

  if (message.role === 'user') {
    return (
      <div className="group flex flex-col items-end gap-1">
        <div className="max-w-[80%] rounded-lg bg-primary text-primary-foreground px-3 py-2 text-sm whitespace-pre-wrap">
          {message.content}
        </div>
        <ActionRow message={message} />
      </div>
    );
  }

  const isStreaming = message.status === 'streaming';
  const failed = message.status === 'failed';
  const hasContent = message.content.length > 0;
  const hasToolCalls = (message.tool_calls?.length ?? 0) > 0;

  return (
    <div className="group flex flex-col gap-2">
      {message.tool_calls?.map((tc) => (
        <ToolCallCard key={tc.id} toolCall={tc} />
      ))}
      {(hasContent || (!hasToolCalls && isStreaming)) && (
        <div className="max-w-[90%] text-sm leading-relaxed space-y-2">
          {hasContent ? <MarkdownBody content={message.content} /> : null}
          {isStreaming && (
            <span className="inline-block w-1.5 h-3.5 align-baseline bg-muted-foreground/60 animate-pulse" />
          )}
        </div>
      )}
      {failed && (
        <div className="text-xs text-destructive">message failed</div>
      )}
      {!isStreaming && <StatsLine message={message} />}
      {!isStreaming && (hasContent || hasToolCalls) && (
        <ActionRow message={message} />
      )}
    </div>
  );
}
