// v2.0.0: BooCoder pane — renders the BooCoder chat + diff interface inside
// BooChat's multi-pane workspace.
//
// Architecture:
// - REST calls go through /api/coder/* which BooChat's server proxies to
//   the boocoder container at http://boocoder:3000/api/*
// - WS connects directly to the boocoder container at :9502 (same Tailscale
//   network, no CORS for WebSocket). In dev, the Vite proxy handles it.

import { useCallback, useEffect, useRef, useState } from 'react';
import { Code, Send, Check, X, RefreshCw } from 'lucide-react';
import { MarkdownRenderer } from '@/components/MarkdownRenderer';
import { cn } from '@/lib/utils';

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface CoderMessage {
  id: string;
  role: 'user' | 'assistant' | 'system';
  content: string;
  status?: 'streaming' | 'complete' | 'failed';
  tool_calls?: Array<{
    id: string;
    function: { name: string; arguments: string };
  }>;
  tool_results?: {
    tool_call_id: string;
    content: string;
  };
}

interface PendingChange {
  id: string;
  file_path: string;
  operation: 'create' | 'modify' | 'delete';
  diff?: string;
  new_content?: string;
  status: 'pending' | 'approved' | 'rejected';
}

interface Props {
  sessionId: string;
}

// ---------------------------------------------------------------------------
// Hooks
// ---------------------------------------------------------------------------

function useCoderMessages(sessionId: string) {
  const [messages, setMessages] = useState<CoderMessage[]>([]);
  const [connected, setConnected] = useState(false);
  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    // Fetch existing messages on mount
    fetch(`/api/coder/sessions/${sessionId}/messages`)
      .then((res) => res.ok ? res.json() : [])
      .then((data: CoderMessage[]) => setMessages(data))
      .catch(() => {/* noop — coder backend may not be running */});
  }, [sessionId]);

  useEffect(() => {
    // WS connects to the coder backend. In production, this goes through the
    // same host (BooChat serves the SPA and proxies). In dev, Vite proxy
    // handles /api/coder/ws/* -> boocoder:9502.
    const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    const wsUrl = `${proto}//${window.location.host}/api/coder/ws/sessions/${sessionId}`;
    const ws = new WebSocket(wsUrl);
    wsRef.current = ws;

    ws.onopen = () => setConnected(true);
    ws.onclose = () => setConnected(false);

    ws.onmessage = (ev) => {
      try {
        const frame = JSON.parse(ev.data as string);
        if (frame.type === 'message_started') {
          setMessages((prev) => [
            ...prev,
            { id: frame.message_id, role: frame.role ?? 'assistant', content: '', status: 'streaming' },
          ]);
        } else if (frame.type === 'delta') {
          setMessages((prev) =>
            prev.map((m) =>
              m.id === frame.message_id
                ? { ...m, content: m.content + (frame.content ?? '') }
                : m
            )
          );
        } else if (frame.type === 'message_complete') {
          setMessages((prev) =>
            prev.map((m) =>
              m.id === frame.message_id ? { ...m, status: 'complete' } : m
            )
          );
        } else if (frame.type === 'tool_call') {
          setMessages((prev) =>
            prev.map((m) =>
              m.id === frame.message_id
                ? {
                    ...m,
                    tool_calls: [
                      ...(m.tool_calls ?? []),
                      { id: frame.tool_call_id, function: { name: frame.name, arguments: frame.arguments ?? '' } },
                    ],
                  }
                : m
            )
          );
        }
      } catch {
        // ignore unparseable frames
      }
    };

    return () => {
      ws.close();
      wsRef.current = null;
    };
  }, [sessionId]);

  return { messages, setMessages, connected };
}

function usePendingChanges(sessionId: string) {
  const [changes, setChanges] = useState<PendingChange[]>([]);
  const [loading, setLoading] = useState(false);

  const refresh = useCallback(() => {
    setLoading(true);
    fetch(`/api/coder/sessions/${sessionId}/pending`)
      .then((res) => res.ok ? res.json() : [])
      .then((data: PendingChange[]) => setChanges(data))
      .catch(() => {/* noop */})
      .finally(() => setLoading(false));
  }, [sessionId]);

  useEffect(() => { refresh(); }, [refresh]);

  const approve = useCallback(async (changeId: string) => {
    const res = await fetch(`/api/coder/sessions/${sessionId}/pending/${changeId}/approve`, {
      method: 'POST',
    });
    if (res.ok) {
      setChanges((prev) => prev.map((c) => c.id === changeId ? { ...c, status: 'approved' } : c));
    }
  }, [sessionId]);

  const reject = useCallback(async (changeId: string) => {
    const res = await fetch(`/api/coder/sessions/${sessionId}/pending/${changeId}/reject`, {
      method: 'POST',
    });
    if (res.ok) {
      setChanges((prev) => prev.map((c) => c.id === changeId ? { ...c, status: 'rejected' } : c));
    }
  }, [sessionId]);

  return { changes, loading, refresh, approve, reject };
}

// ---------------------------------------------------------------------------
// Sub-components
// ---------------------------------------------------------------------------

function CoderMessageBubble({ message }: { message: CoderMessage }) {
  const isUser = message.role === 'user';
  return (
    <div className={cn('flex flex-col gap-1 px-3 py-2', isUser ? 'items-end' : 'items-start')}>
      <div
        className={cn(
          'rounded-lg px-3 py-2 max-w-[85%] text-sm',
          isUser
            ? 'bg-primary text-primary-foreground'
            : 'bg-muted text-foreground'
        )}
      >
        {isUser ? (
          <p className="whitespace-pre-wrap">{message.content}</p>
        ) : (
          <div className="prose prose-sm dark:prose-invert max-w-none">
            <MarkdownRenderer content={message.content} />
          </div>
        )}
        {message.tool_calls && message.tool_calls.length > 0 && (
          <div className="mt-2 border-t border-border/50 pt-2 space-y-1">
            {message.tool_calls.map((tc) => (
              <div key={tc.id} className="text-xs font-mono text-muted-foreground">
                <span className="text-primary/70">{tc.function.name}</span>
                {tc.function.arguments && (
                  <span className="ml-1 opacity-60">
                    ({tc.function.arguments.slice(0, 80)}
                    {tc.function.arguments.length > 80 ? '...' : ''})
                  </span>
                )}
              </div>
            ))}
          </div>
        )}
        {message.status === 'streaming' && (
          <span className="inline-block w-2 h-4 bg-current opacity-60 animate-pulse ml-0.5" />
        )}
      </div>
    </div>
  );
}

function DiffPanel({
  changes,
  loading,
  onRefresh,
  onApprove,
  onReject,
}: {
  changes: PendingChange[];
  loading: boolean;
  onRefresh: () => void;
  onApprove: (id: string) => void;
  onReject: (id: string) => void;
}) {
  const pending = changes.filter((c) => c.status === 'pending');

  return (
    <div className="flex flex-col h-full border-t border-border">
      <div className="flex items-center justify-between px-3 py-1.5 border-b border-border bg-muted/30">
        <span className="text-xs font-medium text-muted-foreground">
          Pending Changes {pending.length > 0 && `(${pending.length})`}
        </span>
        <button
          type="button"
          onClick={onRefresh}
          disabled={loading}
          className="inline-flex items-center justify-center size-6 rounded hover:bg-muted text-muted-foreground"
          aria-label="Refresh pending changes"
        >
          <RefreshCw size={12} className={loading ? 'animate-spin' : ''} />
        </button>
      </div>
      <div className="flex-1 overflow-y-auto">
        {pending.length === 0 ? (
          <div className="flex items-center justify-center h-full text-sm text-muted-foreground">
            No pending changes
          </div>
        ) : (
          <div className="divide-y divide-border">
            {pending.map((change) => (
              <div key={change.id} className="px-3 py-2">
                <div className="flex items-center justify-between mb-1">
                  <span className="text-xs font-mono text-foreground truncate flex-1 mr-2">
                    <span className={cn(
                      'inline-block w-1.5 h-1.5 rounded-full mr-1.5',
                      change.operation === 'create' && 'bg-green-500',
                      change.operation === 'modify' && 'bg-yellow-500',
                      change.operation === 'delete' && 'bg-red-500',
                    )} />
                    {change.file_path}
                  </span>
                  <div className="flex items-center gap-1 shrink-0">
                    <button
                      type="button"
                      onClick={() => onApprove(change.id)}
                      className="inline-flex items-center justify-center size-6 rounded bg-green-500/10 hover:bg-green-500/20 text-green-600 dark:text-green-400"
                      aria-label="Approve change"
                      title="Approve"
                    >
                      <Check size={12} />
                    </button>
                    <button
                      type="button"
                      onClick={() => onReject(change.id)}
                      className="inline-flex items-center justify-center size-6 rounded bg-red-500/10 hover:bg-red-500/20 text-red-600 dark:text-red-400"
                      aria-label="Reject change"
                      title="Reject"
                    >
                      <X size={12} />
                    </button>
                  </div>
                </div>
                {change.diff && (
                  <pre className="text-[11px] font-mono bg-muted/50 rounded p-2 overflow-x-auto max-h-32 whitespace-pre">
                    {change.diff}
                  </pre>
                )}
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------

export function CoderPane({ sessionId }: Props) {
  const { messages, setMessages, connected } = useCoderMessages(sessionId);
  const { changes, loading, refresh, approve, reject } = usePendingChanges(sessionId);
  const [input, setInput] = useState('');
  const [sending, setSending] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLTextAreaElement>(null);

  // Auto-scroll on new messages
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  // Refresh pending changes when a message_complete arrives
  useEffect(() => {
    const lastMsg = messages[messages.length - 1];
    if (lastMsg?.role === 'assistant' && lastMsg.status === 'complete') {
      refresh();
    }
  }, [messages, refresh]);

  const handleSend = useCallback(async () => {
    const text = input.trim();
    if (!text || sending) return;

    setInput('');
    setSending(true);

    // Optimistic user message
    const tempId = `temp-${Date.now()}`;
    setMessages((prev) => [...prev, { id: tempId, role: 'user', content: text, status: 'complete' }]);

    try {
      const res = await fetch(`/api/coder/sessions/${sessionId}/messages`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ content: text }),
      });
      if (res.ok) {
        const data = await res.json();
        // Replace temp message with real one if server returned it
        if (data.user_message_id) {
          setMessages((prev) =>
            prev.map((m) => m.id === tempId ? { ...m, id: data.user_message_id } : m)
          );
        }
      }
    } catch {
      // The WS will bring the real messages; optimistic is good enough
    } finally {
      setSending(false);
    }
  }, [input, sending, sessionId, setMessages]);

  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        void handleSend();
      }
    },
    [handleSend]
  );

  return (
    <div className="flex flex-col h-full bg-background">
      {/* Header */}
      <div className="flex items-center gap-2 px-3 py-1.5 border-b border-border bg-muted/30 shrink-0">
        <Code size={14} className="text-muted-foreground" />
        <span className="text-xs font-medium text-muted-foreground">BooCoder</span>
        <span
          className={cn(
            'inline-block w-1.5 h-1.5 rounded-full ml-auto',
            connected ? 'bg-green-500' : 'bg-red-500'
          )}
          title={connected ? 'Connected' : 'Disconnected'}
        />
      </div>

      {/* Chat area */}
      <div className="flex-1 min-h-0 overflow-y-auto">
        {messages.length === 0 ? (
          <div className="flex flex-col items-center justify-center h-full text-sm text-muted-foreground gap-2">
            <Code size={32} className="opacity-40" />
            <p>Send a message to start coding</p>
          </div>
        ) : (
          <div className="py-2">
            {messages.map((msg) => (
              <CoderMessageBubble key={msg.id} message={msg} />
            ))}
            <div ref={messagesEndRef} />
          </div>
        )}
      </div>

      {/* Diff panel — only shows when there are pending changes */}
      {changes.filter((c) => c.status === 'pending').length > 0 && (
        <div className="h-48 shrink-0">
          <DiffPanel
            changes={changes}
            loading={loading}
            onRefresh={refresh}
            onApprove={approve}
            onReject={reject}
          />
        </div>
      )}

      {/* Input */}
      <div className="shrink-0 border-t border-border p-2">
        <div className="flex items-end gap-2">
          <textarea
            ref={inputRef}
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder="Ask BooCoder to write code..."
            rows={1}
            className="flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring max-h-32 min-h-[36px] max-md:min-h-[44px]"
          />
          <button
            type="button"
            onClick={() => void handleSend()}
            disabled={!input.trim() || sending}
            className="inline-flex items-center justify-center size-9 max-md:min-h-[44px] max-md:min-w-[44px] rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-40 disabled:cursor-not-allowed shrink-0"
            aria-label="Send message"
          >
            <Send size={16} />
          </button>
        </div>
      </div>
    </div>
  );
}
