// One connection per `opencode serve` base URL, for the REST reads the chat list and transcript need.
// The SDK-style `/session/*` route family is used for the per-id reads and writes (feature-complete,
// incl. DELETE) — those answer for any session regardless of project, verified 200 with and without a
// directory header. The LIST is the exception and goes through `/api/session`; see `listSessions`.
//
// This used to also hold a shared SSE subscription (`GET /event`) demultiplexed per session, plus
// `createSession`, `postMessage`, `abort` and `isServerHealthy` — the client half of a serve-based turn
// path that was replaced by `opencode run --dir` subprocesses and had no callers left. Removed
// 2026-08-10; `docs/opencode-serve-path.md` records what it did and what a rebuild would want back,
// because Phase 2 may want exactly this shape again.

class ServerConnection {
  constructor(private readonly baseUrl: string) {}

  // ── Session history (REST; OpenCode's SQLite store is the source of truth) ──

  /**
   * Every session the serve knows, across every project.
   *
   * `GET /session` — the endpoint this used — answers for ONE project: the one the request's directory
   * resolves to, which with no `x-opencode-directory` header is the serve's own cwd
   * (`DATA_PATH/opencode_server`). That directory is not a git checkout, so it resolves to the catch-all
   * project `global`, and so does every other non-git directory — which is why the general chat dir
   * listed fine and nothing looked wrong. A cwd that IS a git checkout gets its own project and its
   * sessions were simply never returned. Measured on the live serve: `/session` 8, `/api/session` 13,
   * and a session created in a git directory came back 0 times from `/session` and 1 from `/api/session`.
   *
   * Chat pwds are project directories, so that is the ordinary case, not the edge one.
   *
   * The two surfaces disagree about the shape, and the difference is silent: `/session` carries the
   * working directory as top-level `directory`, `/api/session` as `location.directory` with no top-level
   * field at all, wrapped in `{data: …}`. Normalised here so callers keep reading `directory` — reading
   * the wrong one yields `undefined` for every session, which filters the list down to nothing.
   */
  async listSessions(): Promise<OpenCodeSessionInfo[]> {
    const res = await fetch(`${this.baseUrl}/api/session`);
    if (!res.ok) throw new Error(`opencode GET /api/session → ${res.status}`);
    const body = (await res.json()) as { data?: ApiSessionInfo[] } | ApiSessionInfo[];
    const sessions = Array.isArray(body) ? body : (body.data ?? []);
    return sessions.map((s) => ({ ...s, directory: s.directory ?? s.location?.directory ?? null }));
  }

  /** One session's own record. The only place its working directory can be read on resume. */
  async getSession(sessionId: string): Promise<OpenCodeSessionInfo | null> {
    const res = await fetch(`${this.baseUrl}/session/${sessionId}`);
    if (!res.ok) return null;
    return (await res.json()) as OpenCodeSessionInfo;
  }

  async getMessages(sessionId: string): Promise<OpenCodeStoredMessage[]> {
    const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`);
    if (!res.ok) throw new Error(`opencode GET /session/${sessionId}/message → ${res.status}`);
    return (await res.json()) as OpenCodeStoredMessage[];
  }

  async deleteSession(sessionId: string): Promise<boolean> {
    const res = await fetch(`${this.baseUrl}/session/${sessionId}`, { method: 'DELETE' });
    return res.ok;
  }

  async renameSession(sessionId: string, title: string): Promise<boolean> {
    const res = await fetch(`${this.baseUrl}/session/${sessionId}`, {
      method: 'PATCH',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ title }),
    });
    return res.ok;
  }
}

// Shapes returned by the `/session/*` read endpoints.
//
// Re-verified against the running opencode 1.17.9 on 2026-08-10 by reading `GET /session` directly. The
// keys it actually returns are:
//
//   id, slug, projectID, directory, path, summary, cost, tokens, title, agent, model, version, time,
//   permission
//
// Two fields this type used to declare are NOT among them, and both were load-bearing:
//
//   - `location.directory` — there is no `location` object. The working directory is top-level
//     `directory`. Every read of `location?.directory ?? ''` therefore produced '', which is why a
//     resumed OpenCode session lost its cwd.
//   - `metadata` — never returned, and nothing writes it. The session list filtered on
//     `metadata.officer.cwd`, so it matched nothing and no OpenCode session was ever listed.
//
// Kept deliberately narrow: only the fields we read. Adding one means confirming it against a live
// server first — the previous pair were plausible and wrong, and cost two user-visible defects.
export type OpenCodeSessionInfo = {
  id: string;
  title?: string;
  time?: { created?: number; updated?: number };
  /** Absolute working directory the session belongs to. Top-level; there is no `location` wrapper. */
  directory?: string | null;
};

/** `/api/session`'s shape: the same record, with the directory one level down. Normalised by `listSessions`. */
type ApiSessionInfo = OpenCodeSessionInfo & { location?: { directory?: string | null } };

export type OpenCodeStoredPart = {
  type?: string;
  text?: string;
  tool?: string;
  callID?: string;
  state?: { input?: Record<string, unknown>; output?: string; error?: string };
};

export type OpenCodeStoredMessage = {
  info?: { role?: string; modelID?: string; model?: { modelID?: string; providerID?: string } };
  parts?: OpenCodeStoredPart[];
};

const connections = new Map<string, ServerConnection>();

export function getConnection(baseUrl: string): ServerConnection {
  let conn = connections.get(baseUrl);
  if (!conn) {
    conn = new ServerConnection(baseUrl);
    connections.set(baseUrl, conn);
  }
  return conn;
}

export type { ServerConnection };
