// @ts-check
/**
 * Chat — the WEB presenter for the `chat` builtin view (React + Tailwind).
 * Browser-only: consumed solely by the web serve path; NEVER Node-imported.
 * The default export is a pure function of `state`; DOM events call
 * `dispatch(intentName, payload?)`.
 *
 * @module chat/web
 */

import { Loading, Empty, ErrorState, NotReady } from '@crouton-kit/crouter/web';
import { messageText } from './core.mjs';

/** @typedef {import('./core.mjs').ChatState} ChatState */

const NOTICE_STYLES = {
  info: 'border-slate-200 bg-slate-50 text-slate-700',
  action: 'border-amber-200 bg-amber-50 text-amber-800',
  error: 'border-rose-200 bg-rose-50 text-rose-800',
};

const LEVEL_BADGES = {
  info: 'bg-slate-100 text-slate-700',
  action: 'bg-amber-100 text-amber-800',
  error: 'bg-rose-100 text-rose-800',
};

/** @param {unknown} value @returns {value is Record<string, unknown>} */
function isRecord(value) {
  return typeof value === 'object' && value !== null;
}

/** @param {string} text @param {number} max */
function truncate(text, max = 120) {
  const value = String(text || '');
  return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}…` : value;
}

/** @param {unknown} message */
function messageRole(message) {
  return isRecord(message) && typeof message.role === 'string' ? message.role : '';
}

/** @param {unknown} message */
function messageKind(message) {
  if (!isRecord(message)) return '';
  if (typeof message.customType === 'string' && message.customType) return `custom · ${message.customType}`;
  if (typeof message.type === 'string' && message.type) return message.type;
  return '';
}

/** @param {unknown} message */
function messageTs(message) {
  if (!isRecord(message)) return '';
  if (typeof message.timestamp === 'string') return message.timestamp;
  if (typeof message.timestamp === 'number') return new Date(message.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
  return '';
}

/** @param {unknown} value */
function blockText(value) {
  if (!isRecord(value)) return '';
  if (typeof value.text === 'string') return value.text;
  if (Array.isArray(value.text)) return value.text.map((part) => (isRecord(part) && typeof part.text === 'string' ? part.text : '')).join('');
  if (typeof value.content === 'string') return value.content;
  return '';
}

/** @param {unknown} value */
function summarizeBlock(value) {
  if (!isRecord(value)) return '';
  const type = typeof value.type === 'string' ? value.type : '';
  const name = typeof value.name === 'string' ? value.name : '';
  switch (type) {
    case 'text':
      return '';
    case 'thinking':
    case 'reasoning': {
      const text = truncate(blockText(value).replace(/\s+/g, ' ').trim(), 88);
      return text ? `Thinking · ${text}` : 'Thinking';
    }
    case 'toolCall': {
      const args = blockText(value);
      const tail = args ? ` · ${truncate(args.replace(/\s+/g, ' ').trim(), 88)}` : '';
      return `Tool · ${name || 'tool'}${tail}`;
    }
    case 'toolResult': {
      const result = blockText(value);
      const tail = result ? ` · ${truncate(result.replace(/\s+/g, ' ').trim(), 88)}` : '';
      return `Tool result${tail}`;
    }
    default:
      return type ? `${type}${name ? ` · ${name}` : ''}` : '';
  }
}

/** @param {unknown} message */
function messageSummaries(message) {
  const summaries = [];
  if (!isRecord(message)) return summaries;
  if (messageRole(message) === 'bashExecution') {
    const command = typeof message.command === 'string' ? message.command : '';
    if (command) summaries.push(`$ ${truncate(command.replace(/\s+/g, ' ').trim(), 96)}`);
    if (typeof message.exitCode === 'number') summaries.push(`exit ${message.exitCode}`);
    if (message.cancelled === true) summaries.push('cancelled');
    if (message.truncated === true) summaries.push('truncated');
  }

  const content = Array.isArray(message.content) ? message.content : [];
  for (const block of content) {
    const summary = summarizeBlock(block);
    if (summary) summaries.push(summary);
  }

  const kind = messageKind(message);
  if (kind && messageRole(message) === 'custom') summaries.push(kind);
  if (messageRole(message) === 'compactionSummary') summaries.push('Compaction summary');
  if (messageRole(message) === 'branchSummary') summaries.push('Branch summary');
  return summaries;
}

/** @param {ChatState} state */
function connectionGuidance(state) {
  const target = state.target || '<node-id>';
  switch (state.conn) {
    case 'no-broker':
      return {
        level: 'action',
        Comp: NotReady,
        headline: 'node is asleep',
        explanation: 'Focus or revive the node first, then reconnect.',
        nextStep: `crtr surface node focus ${target} · or crtr node lifecycle revive ${target}`,
      };
    case 'no-node':
      return {
        level: 'error',
        Comp: ErrorState,
        headline: 'node not found',
        explanation: 'Check the target node id and try again.',
        nextStep: `crtr view run chat --target ${target}`,
      };
    case 'invalid':
      return {
        level: 'error',
        Comp: ErrorState,
        headline: 'invalid target',
        explanation: 'The target was rejected by the broker.',
        nextStep: `crtr view run chat --target <node-id>`,
      };
    case 'error':
      if (!state.target) {
        return {
          level: 'error',
          Comp: ErrorState,
          headline: 'missing target',
          explanation: 'Open chat with a node id so the view can connect.',
          nextStep: 'crtr view run chat --target <node-id>',
        };
      }
      return {
        level: 'error',
        Comp: ErrorState,
        headline: 'chat connection failed',
        explanation: 'Reconnect to retry the stream.',
        nextStep: 'Press Reconnect',
      };
    default:
      return null;
  }
}

/** @param {{ label: string, value: string, cls?: string }} props */
function Pill({ label, value, cls = '' }) {
  return (
    <span className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] ${cls}`}>
      <span className="opacity-60">{label}</span>
      <span className="font-semibold">{value}</span>
    </span>
  );
}

/** @param {{ notice: ChatState['notices'][number], onDismiss: (id: string) => void }} props */
function NoticeRow({ notice, onDismiss }) {
  return (
    <div className={`flex items-start gap-3 rounded-xl border px-3 py-2 text-sm ${NOTICE_STYLES[notice.level] || NOTICE_STYLES.info}`}>
      <span className={`rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${LEVEL_BADGES[notice.level] || LEVEL_BADGES.info}`}>{notice.level}</span>
      <div className="min-w-0 flex-1 whitespace-pre-wrap">{notice.text}</div>
      <button type="button" className="shrink-0 text-slate-400 hover:text-slate-700" onClick={() => onDismiss(notice.id)} aria-label="Dismiss notice">
        ×
      </button>
    </div>
  );
}

/** @param {{ message: unknown, index: number, streaming: boolean }} props */
function TranscriptRow({ message, index, streaming }) {
  const role = messageRole(message);
  const user = role === 'user';
  const assistant = role === 'assistant';
  const bubbleCls = user ? 'border-emerald-200 bg-emerald-50' : assistant ? 'border-cyan-200 bg-cyan-50' : 'border-slate-200 bg-white';
  const labelCls = user ? 'text-emerald-700' : assistant ? 'text-cyan-700' : 'text-slate-700';
  const text = messageText(message).trim();
  const summaries = messageSummaries(message);
  const ts = messageTs(message);

  return (
    <article className={`rounded-2xl border px-3 py-2 shadow-sm ${bubbleCls}`}>
      <div className="flex items-baseline gap-2">
        <div className={`font-semibold ${labelCls}`}>{user ? 'You' : assistant ? 'Agent' : role || 'Message'}</div>
        {streaming ? <span className="rounded-full bg-cyan-100 px-2 py-0.5 text-[11px] font-semibold text-cyan-800">streaming</span> : null}
        {role === 'bashExecution' ? <span className="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-semibold text-slate-700">bash</span> : null}
        {ts ? <div className="ml-auto text-xs text-slate-400">{ts}</div> : null}
      </div>
      {text ? <div className="mt-1 whitespace-pre-wrap text-slate-800">{text}</div> : null}
      {summaries.length ? (
        <div className="mt-2 space-y-1 border-t border-slate-200 pt-2 text-xs text-slate-500">
          {summaries.map((summary, i) => (
            <div key={`${index}:${i}`} className="whitespace-pre-wrap pl-3 text-slate-500">
              • {summary}
            </div>
          ))}
        </div>
      ) : null}
    </article>
  );
}

/** @param {{ state: ChatState, dispatch: (intent: string, payload?: unknown) => void }} props */
export default function Chat({ state, dispatch }) {
  const guidance = connectionGuidance(state);
  if (state.target === null) {
    return (
      <div className="flex h-full items-center justify-center p-6 text-sm text-slate-500">
        <NotReady
          headline="missing target"
          explanation="Open chat with a node id so the view can connect."
          nextStep="crtr view run chat --target <node-id>"
          onRetry={() => dispatch('reconnect')}
        />
      </div>
    );
  }
  if (guidance) {
    const Comp = guidance.Comp;
    return (
      <div className="flex h-full items-center justify-center p-6 text-sm text-slate-500">
        <Comp
          headline={guidance.headline}
          explanation={guidance.explanation}
          nextStep={guidance.nextStep}
          onRetry={() => dispatch('reconnect')}
        />
      </div>
    );
  }

  const connLabel = state.conn === 'open' ? 'connected' : state.conn === 'connecting' ? 'connecting…' : state.conn;
  const sessionName = state.session?.sessionName || '';
  const model = state.session?.model || '';
  const streaming = state.session?.isStreaming === true;
  const canSend = state.channel != null && state.conn === 'open' && state.role === 'controller' && state.draft.trim() !== '';
  const canAbort = state.channel != null && state.conn === 'open' && state.role === 'controller' && streaming;
  const controlLabel = state.role === 'controller' ? 'Release control' : 'Request control';
  const controlIntent = state.role === 'controller' ? 'releaseControl' : 'requestControl';
  const placeholder = state.conn !== 'open'
    ? 'Waiting for connection…'
    : state.role === 'observer'
      ? 'Take control before sending…'
      : streaming
        ? 'Type a steer…'
        : 'Type a message…';

  return (
    <div className="flex h-full min-h-0 flex-col gap-3 font-mono text-sm text-slate-900 outline-none">
      <div className="flex flex-wrap items-center gap-2 rounded-2xl border border-slate-200 bg-white px-3 py-2 shadow-sm">
        <Pill label="target" value={state.target || '—'} cls="border-slate-200 bg-slate-50 text-slate-700" />
        <Pill label="conn" value={connLabel} cls={state.conn === 'open' ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : state.conn === 'connecting' ? 'border-amber-200 bg-amber-50 text-amber-800' : 'border-slate-200 bg-slate-50 text-slate-700'} />
        <Pill label="role" value={state.role} cls={state.role === 'controller' ? 'border-cyan-200 bg-cyan-50 text-cyan-700' : 'border-slate-200 bg-slate-50 text-slate-700'} />
        {model ? <Pill label="model" value={model} cls="border-violet-200 bg-violet-50 text-violet-700" /> : null}
        {sessionName ? <Pill label="session" value={sessionName} cls="border-slate-200 bg-slate-50 text-slate-700" /> : null}
        {state.controllerId ? <Pill label="controller" value={state.controllerId} cls="border-slate-200 bg-slate-50 text-slate-700" /> : null}
        {typeof state.contextTokens === 'number' ? <Pill label="tokens" value={String(state.contextTokens)} cls="border-slate-200 bg-slate-50 text-slate-700" /> : null}
        {state.transcript.activity ? <Pill label="activity" value={state.transcript.activity} cls="border-amber-200 bg-amber-50 text-amber-800" /> : null}
        {state.queued.length ? <Pill label="queued" value={state.queued.join(' · ')} cls="border-slate-200 bg-slate-50 text-slate-700" /> : null}
        <div className="ml-auto flex flex-wrap items-center gap-2">
          <button type="button" className="rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50" onClick={() => dispatch('reconnect')}>
            Reconnect
          </button>
          <button type="button" className="rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50" onClick={() => dispatch(controlIntent)}>
            {controlLabel}
          </button>
        </div>
      </div>

      {state.notices.length ? (
        <div className="space-y-2">
          {state.notices.map((notice) => (
            <NoticeRow key={notice.id} notice={notice} onDismiss={(id) => dispatch('clearNotice', id)} />
          ))}
        </div>
      ) : null}

      <div className="min-h-0 flex-1 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
        <div className="flex h-full min-h-0 flex-col">
          <div className="border-b border-slate-200 px-3 py-2 text-xs uppercase tracking-[0.18em] text-slate-400">
            Transcript
          </div>
          <div className="min-h-0 flex-1 space-y-3 overflow-y-auto p-3">
            {state.transcript.messages.length === 0 ? (
              state.conn === 'connecting' ? (
                <Loading label="Connecting to chat…" />
              ) : state.conn === 'open' ? (
                <Empty label="No messages yet." />
              ) : (
                <div className="flex h-full items-center justify-center text-slate-400">Waiting for welcome…</div>
              )
            ) : (
              state.transcript.messages.map((message, index) => (
                <TranscriptRow
                  key={index}
                  message={message}
                  index={index}
                  streaming={index === state.transcript.streamingIndex && streaming}
                />
              ))
            )}
          </div>
        </div>
      </div>

      <div className="rounded-2xl border border-slate-200 bg-white p-3 shadow-sm">
        <textarea
          value={state.draft}
          onChange={(e) => dispatch('setDraft', e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
              e.preventDefault();
              dispatch('submitDraft');
            }
          }}
          placeholder={placeholder}
          rows={3}
          className="w-full resize-none rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-sm outline-none focus:border-slate-400"
        />
        <div className="mt-3 flex flex-wrap items-center gap-2">
          <button
            type="button"
            className={`rounded-lg px-3 py-1.5 text-sm font-medium ${canSend ? 'bg-slate-900 text-white hover:bg-slate-800' : 'cursor-not-allowed bg-slate-200 text-slate-500'}`}
            onClick={() => dispatch('submitDraft')}
            disabled={!canSend}
          >
            Send
          </button>
          <button
            type="button"
            className={`rounded-lg border px-3 py-1.5 text-sm ${canAbort ? 'border-rose-200 bg-rose-50 text-rose-700 hover:bg-rose-100' : 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400'}`}
            onClick={() => dispatch('abort')}
            disabled={!canAbort}
          >
            Stop
          </button>
          <div className="ml-auto text-xs text-slate-400">Enter sends · Shift+Enter inserts a newline</div>
        </div>
      </div>
    </div>
  );
}
