import type { ClaudeSessionSummary, ClaudeSessionDetail, ClaudeChatMessage } from './claude-sessions';
import { ensureServer } from './opencode/server-manager';
import { getConnection } from './opencode/client';
import { logger } from './logger';

// The OpenCode analog of claude-sessions.ts. OpenCode's own store is the source of truth, read via the
// fixed pm2-managed server's HTTP API (never the DB directly). Returns the same shapes as the Claude
// reader, tagged harness:'opencode', so chat.ts can merge both harnesses transparently.
//
// Sessions do NOT all live in one project. Each records the directory its turn ran in — the serve takes
// it per request as `x-opencode-directory` — and one serve holds sessions for many. That is why the cwd
// filter has to read `directory` rather than assume, and why the list has to come from a read that spans
// projects (`client.listSessions`, which is where the project-scoping trap is written up).

/**
 * List OpenCode sessions for a working directory, so each context (/chat pwd, email account, project)
 * sees only its own. With no cwd, returns all. Never throws — returns [] if the server is unavailable.
 *
 * Filters on the session's own `directory`, which is what OpenCode records from the directory the turn
 * ran in. It used to filter on `metadata.officer.cwd`, a tag whose only writer
 * (`client.createSession`) has no callers — so the comparison was against `undefined` for every session
 * and the list was ALWAYS empty. `cwdOf` in chat.ts substitutes a default when no `?cwd=` is given, so
 * the `!cwd` escape never fired either and there was no configuration in which an OpenCode session
 * appeared in /chat. Verified against the live server: 7 sessions present, 0 returned.
 */
/**
 * OpenCode's own placeholder name for a session it has not titled yet.
 *
 * It titles a session from the conversation, but ASYNCHRONOUSLY and after there is something to read —
 * a finished turn ends up called "Single color in oc-red2.png", which is better than anything we would
 * generate. Until then the session is literally named `New session - 2026-08-10T15:44:17.178Z`.
 *
 * That window is exactly when a session is most visible: `/chat/live` shows turns that are RUNNING, so
 * the placeholder is what the panel catches. Treating it as "not yet titled" lets the good name arrive
 * on its own. Forcing `--title` on the run instead would fix the transient case by permanently
 * replacing opencode's title with a truncated prompt — worse where it lasts longest.
 */
export const isPlaceholderOpenCodeTitle = (title: string | undefined): boolean => {
  const trimmed = title?.trim();
  return !trimmed || /^New session - \d{4}-\d{2}-\d{2}T[\d:.]+Z?$/.test(trimmed);
};

export async function listOpenCodeSessions(cwd?: string): Promise<ClaudeSessionSummary[]> {
  try {
    const { baseUrl } = await ensureServer();
    const sessions = await getConnection(baseUrl).listSessions();
    return sessions
      .filter((s) => !cwd || s.directory === cwd)
      .map((s) => {
        const created = s.time?.created ?? Date.now();
        const updated = s.time?.updated ?? created;
        return {
          id: s.id,
          title: isPlaceholderOpenCodeTitle(s.title) ? '(untitled)' : (s.title ?? '(untitled)'),
          cwd: s.directory ?? '',
          createdAt: new Date(created).toISOString(),
          updatedAt: new Date(updated).toISOString(),
          messageCount: 0, // the session list endpoint doesn't include a turn count
          harness: 'opencode',
        } satisfies ClaudeSessionSummary;
      });
  } catch (err) {
    logger.warn('Failed to list OpenCode sessions', { error: String(err) });
    return [];
  }
}

/** Load one OpenCode session's transcript, rebuilt into the shared display message shape. */
export async function loadOpenCodeSession(sessionId: string): Promise<ClaudeSessionDetail | null> {
  try {
    const { baseUrl } = await ensureServer();
    const conn = getConnection(baseUrl);
    // Both reads, together: the transcript, and the session record that carries its directory. OpenCode
    // needs the cwd on EVERY turn (it rebuilds its working-directory system prompt each time), so a
    // resume that reports '' silently relocates the conversation to the default chat dir.
    const [stored, info] = await Promise.all([conn.getMessages(sessionId), conn.getSession(sessionId)]);

    const messages: ClaudeChatMessage[] = [];
    let modelId = '';

    for (const m of stored) {
      const role = m.info?.role;
      const parts = m.parts ?? [];

      if (role === 'user') {
        const text = parts
          .filter((p) => p.type === 'text')
          .map((p) => p.text ?? '')
          .join('')
          .trim();
        if (text) messages.push({ role: 'user', text });
        continue;
      }

      if (role === 'assistant') {
        if (!modelId) modelId = m.info?.model?.modelID ?? m.info?.modelID ?? '';
        for (const p of parts) {
          if (p.type === 'text' && p.text) {
            messages.push({ role: 'assistant', id: `${sessionId}-${messages.length}`, text: p.text });
          } else if (p.type === 'tool' && p.callID) {
            messages.push({
              role: 'tool',
              toolName: p.tool ?? 'tool',
              toolInput: p.state?.input ?? {},
              toolCallId: p.callID,
              output: p.state?.output,
              isError: Boolean(p.state?.error),
            });
          }
          // skip reasoning/step-* parts (parity with the streaming delta filter).
        }
      }
    }

    return {
      id: sessionId,
      model: modelId ? `opencode/${modelId}` : 'opencode',
      cwd: info?.directory ?? '',
      messages,
    };
  } catch (err) {
    logger.warn('Failed to load OpenCode session', { sessionId, error: String(err) });
    return null;
  }
}

export async function deleteOpenCodeSession(sessionId: string): Promise<boolean> {
  try {
    const { baseUrl } = await ensureServer();
    return await getConnection(baseUrl).deleteSession(sessionId);
  } catch (err) {
    logger.warn('Failed to delete OpenCode session', { sessionId, error: String(err) });
    return false;
  }
}

export async function renameOpenCodeSession(sessionId: string, title: string): Promise<boolean> {
  try {
    const { baseUrl } = await ensureServer();
    return await getConnection(baseUrl).renameSession(sessionId, title);
  } catch (err) {
    logger.warn('Failed to rename OpenCode session', { sessionId, error: String(err) });
    return false;
  }
}

/** OpenCode session ids are prefixed `ses_`; Claude ids are bare UUIDs. */
export const isOpenCodeSessionId = (id: string): boolean => id.startsWith('ses_');
