// Client REST typé du daemon Arboretum : module pur (pas d'import vscode), testable.
// Auth par en-tête `Authorization: Bearer <token>` (le hook preValidation du serveur l'accepte
// au même titre que le cookie). Un client Node n'envoie pas d'en-tête Origin → il passe le check
// Origin strict côté serveur. Réutilise les types de `@arboretum/shared`.
import type {
  ApiError,
  CommitWorktreeRequest,
  CreateGroupSessionRequest,
  CreateSessionRequest,
  CreateWorktreeRequest,
  CreateWorktreeResponse,
  FetchWorktreeRequest,
  GroupSessionResponse,
  GroupsListResponse,
  MeResponse,
  PromoteWorktreeRequest,
  PullWorktreeRequest,
  PushWorktreeRequest,
  RepoBranchesResponse,
  ReposListResponse,
  SessionResponse,
  SessionsListResponse,
  StartRepoSessionRequest,
  WorktreeResponse,
  WorktreesListResponse,
} from '@arboretum/shared';
import { normalizeBaseUrl } from '../config.js';

/** Erreur normalisée portant le code/HTTP renvoyés par le serveur. */
export class RestError extends Error {
  constructor(
    message: string,
    readonly status: number,
    readonly code: string,
  ) {
    super(message);
    this.name = 'RestError';
  }
}

type FetchLike = typeof fetch;

export class RestClient {
  private baseUrl: string;
  private token: string | null;

  constructor(
    opts: { baseUrl: string; token?: string | null },
    private readonly fetchImpl: FetchLike = fetch,
  ) {
    this.baseUrl = normalizeBaseUrl(opts.baseUrl);
    this.token = opts.token ?? null;
  }

  setBaseUrl(url: string): void {
    this.baseUrl = normalizeBaseUrl(url);
  }

  setToken(token: string | null): void {
    this.token = token;
  }

  get url(): string {
    return this.baseUrl;
  }

  // ---- auth ----
  /** Valide le token courant ; lève RestError(401) si invalide. */
  me(): Promise<MeResponse> {
    return this.request<MeResponse>('GET', '/api/v1/auth/me');
  }

  // ---- repos & worktrees ----
  listRepos(): Promise<ReposListResponse> {
    return this.request<ReposListResponse>('GET', '/api/v1/repos');
  }

  listWorktrees(): Promise<WorktreesListResponse> {
    return this.request<WorktreesListResponse>('GET', '/api/v1/worktrees');
  }

  getBranches(repoId: string): Promise<RepoBranchesResponse> {
    return this.request<RepoBranchesResponse>('GET', `/api/v1/repos/${encodeURIComponent(repoId)}/branches`);
  }

  createWorktree(repoId: string, body: CreateWorktreeRequest): Promise<CreateWorktreeResponse> {
    return this.request<CreateWorktreeResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees`, body);
  }

  commitWorktree(repoId: string, body: CommitWorktreeRequest): Promise<WorktreeResponse> {
    return this.request<WorktreeResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/commit`, body);
  }

  pushWorktree(repoId: string, body: PushWorktreeRequest): Promise<WorktreeResponse> {
    return this.request<WorktreeResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/push`, body);
  }

  fetchWorktree(repoId: string, body: FetchWorktreeRequest): Promise<WorktreeResponse> {
    return this.request<WorktreeResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/fetch`, body);
  }

  pullWorktree(repoId: string, body: PullWorktreeRequest): Promise<WorktreeResponse> {
    return this.request<WorktreeResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/pull`, body);
  }

  promoteWorktree(repoId: string, body: PromoteWorktreeRequest): Promise<WorktreeResponse> {
    return this.request<WorktreeResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/promote`, body);
  }

  startRepoSession(repoId: string, body: StartRepoSessionRequest): Promise<SessionResponse> {
    return this.request<SessionResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/session`, body);
  }

  // ---- sessions ----
  createSession(body: CreateSessionRequest): Promise<SessionResponse> {
    return this.request<SessionResponse>('POST', '/api/v1/sessions', body);
  }

  listSessions(opts: { includeHidden?: boolean; includeArchived?: boolean } = {}): Promise<SessionsListResponse> {
    const params = new URLSearchParams();
    if (opts.includeHidden) params.set('includeHidden', 'true');
    if (opts.includeArchived) params.set('includeArchived', 'true');
    const q = params.toString();
    return this.request<SessionsListResponse>('GET', `/api/v1/sessions${q ? `?${q}` : ''}`);
  }

  resumeSession(id: string): Promise<SessionResponse> {
    return this.request<SessionResponse>('POST', `/api/v1/sessions/${encodeURIComponent(id)}/resume`, {});
  }

  forkSession(id: string): Promise<SessionResponse> {
    return this.request<SessionResponse>('POST', `/api/v1/sessions/${encodeURIComponent(id)}/fork`, {});
  }

  hideSession(id: string): Promise<{ ok: true }> {
    return this.request<{ ok: true }>('POST', `/api/v1/sessions/${encodeURIComponent(id)}/hide`, {});
  }

  killSession(id: string): Promise<{ ok: true }> {
    return this.request<{ ok: true }>('DELETE', `/api/v1/sessions/${encodeURIComponent(id)}`);
  }

  // ---- groupes ----
  listGroups(): Promise<GroupsListResponse> {
    return this.request<GroupsListResponse>('GET', '/api/v1/groups');
  }

  createGroupSession(groupId: string, body: CreateGroupSessionRequest): Promise<GroupSessionResponse> {
    return this.request<GroupSessionResponse>('POST', `/api/v1/groups/${encodeURIComponent(groupId)}/session`, body);
  }

  // ---- interne ----
  private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
    const headers: Record<string, string> = { Accept: 'application/json' };
    if (this.token) headers.Authorization = `Bearer ${this.token}`;
    if (body !== undefined) headers['Content-Type'] = 'application/json';

    const init: RequestInit = { method, headers };
    if (body !== undefined) init.body = JSON.stringify(body);

    let res: Response;
    try {
      res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
    } catch (err) {
      throw new RestError(`Cannot reach Arboretum at ${this.baseUrl}: ${(err as Error).message}`, 0, 'NETWORK');
    }

    if (!res.ok) {
      let code = `HTTP_${res.status}`;
      let message = `${method} ${path} → ${res.status}`;
      try {
        const data = (await res.json()) as ApiError;
        if (data?.error) {
          code = data.error.code ?? code;
          message = data.error.message ?? message;
        }
      } catch {
        /* corps non-JSON : on garde le message générique */
      }
      throw new RestError(message, res.status, code);
    }

    if (res.status === 204) return undefined as T;
    return (await res.json()) as T;
  }
}
