/**
 * Typed client for the LogicSRC team credential-sharing API (commandboard-api,
 * routes under /api/credshare). Used by both the `team` credential provider and
 * the CLI `teams`/`login` commands.
 *
 * All secret material sent through this client is already ciphertext (or a DEK
 * sealed to a member public key). The server is zero-knowledge for values.
 */

export interface TeamClientOptions {
  apiUrl: string;
  token?: string;
}

export interface RemoteUser {
  id: string;
  email: string;
  publicKey: string | null;
}

export interface RemoteTeam {
  id: string;
  slug: string;
  name: string;
}

export interface RemoteMember {
  id: string;
  email: string;
  role: "owner" | "admin" | "member";
  status: "active" | "invited";
  hasPublicKey: boolean;
  joinedAt: string | null;
}

export interface RemoteVault {
  id: string;
  name: string;
  hasAccess: boolean;
  secretCount: number;
}

export interface RemoteSecret {
  name: string;
  nonce: string;
  ciphertext: string;
  fingerprint: string;
  version: number;
  updatedAt: string;
}

export interface RemoteGrantRow {
  email: string;
  /** X25519 public key, so a client can re-seal to every member in one pass. */
  publicKey: string | null;
  status: "active" | "invited";
  hasPublicKey: boolean;
  hasAccess: boolean;
}

export interface RekeyResult {
  ok: boolean;
  rekeyed: number;
  granted: string[];
  revoked: string[];
}

export class TeamApiError extends Error {
  constructor(
    public readonly status: number,
    message: string
  ) {
    super(message);
    this.name = "TeamApiError";
  }
}

export class TeamClient {
  private readonly apiUrl: string;
  private token?: string;

  constructor(options: TeamClientOptions) {
    this.apiUrl = options.apiUrl.replace(/\/$/, "");
    this.token = options.token;
  }

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

  private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
    const headers: Record<string, string> = { accept: "application/json" };
    if (body !== undefined) headers["content-type"] = "application/json";
    if (this.token) headers["authorization"] = `Bearer ${this.token}`;
    const response = await fetch(`${this.apiUrl}/api/credshare${path}`, {
      method,
      headers,
      body: body === undefined ? undefined : JSON.stringify(body)
    });
    const text = await response.text();
    const parsed = text ? (JSON.parse(text) as unknown) : undefined;
    if (!response.ok) {
      const message = isRecord(parsed) && typeof parsed.error === "string" ? parsed.error : `${response.status} ${response.statusText}`;
      throw new TeamApiError(response.status, message);
    }
    return parsed as T;
  }

  // ---- auth ----
  requestLoginCode(email: string) {
    return this.request<{ ok: boolean; emailSent: boolean; devCode?: string }>("POST", "/auth/request", { email });
  }
  verifyLoginCode(email: string, code: string) {
    return this.request<{ token: string; user: RemoteUser }>("POST", "/auth/verify", { email, code });
  }
  uploadPublicKey(publicKey: string) {
    return this.request<{ email: string; publicKey: string }>("POST", "/keys", { publicKey });
  }
  me() {
    return this.request<{ user: RemoteUser; teams: RemoteTeam[] }>("GET", "/me");
  }
  logout() {
    return this.request<{ ok: boolean }>("POST", "/logout");
  }
  lookupUser(email: string) {
    return this.request<{ email: string; userId: string | null; publicKey: string | null }>("GET", `/users?email=${encodeURIComponent(email)}`);
  }

  // ---- teams / members / invites ----
  createTeam(slug: string, name?: string) {
    return this.request<{ team: RemoteTeam }>("POST", "/teams", { slug, name });
  }
  listTeams() {
    return this.request<{ teams: RemoteTeam[] }>("GET", "/teams");
  }
  listMembers(slug: string) {
    return this.request<{ members: RemoteMember[] }>("GET", `/teams/${encodeURIComponent(slug)}/members`);
  }
  invite(slug: string, email: string, role?: "owner" | "admin" | "member") {
    return this.request<{ invite: { id: string; email: string; role: string; expiresAt: number }; emailSent: boolean; resent: boolean; token?: string }>(
      "POST",
      `/teams/${encodeURIComponent(slug)}/invites`,
      { email, role }
    );
  }
  updateMemberRole(slug: string, memberId: string, role: "owner" | "admin" | "member") {
    return this.request<{ member: Pick<RemoteMember, "id" | "email" | "role" | "status"> }>(
      "PATCH",
      `/teams/${encodeURIComponent(slug)}/members/${encodeURIComponent(memberId)}`,
      { role }
    );
  }
  removeMember(slug: string, memberId: string) {
    return this.request<{ ok: boolean; removed: string; revokedVaultGrants: number; rotationRequired: boolean }>(
      "DELETE",
      `/teams/${encodeURIComponent(slug)}/members/${encodeURIComponent(memberId)}`
    );
  }
  acceptInvite(token: string) {
    return this.request<{ ok: boolean; team?: RemoteTeam }>("POST", "/invites/accept", { token });
  }

  // ---- vaults / grants / secrets ----
  listVaults(slug: string) {
    return this.request<{ vaults: RemoteVault[] }>("GET", `/teams/${encodeURIComponent(slug)}/vaults`);
  }
  createVault(slug: string, name: string) {
    return this.request<{ vault: { id: string; name: string } }>("POST", `/teams/${encodeURIComponent(slug)}/vaults`, { name });
  }
  getMyGrant(vaultId: string) {
    return this.request<{ wrappedDek: string }>("GET", `/vaults/${encodeURIComponent(vaultId)}/grant`);
  }
  listGrants(vaultId: string) {
    return this.request<{ grants: RemoteGrantRow[] }>("GET", `/vaults/${encodeURIComponent(vaultId)}/grants`);
  }
  putGrant(vaultId: string, email: string, wrappedDek: string) {
    return this.request<{ ok: boolean }>("POST", `/vaults/${encodeURIComponent(vaultId)}/grants`, { email, wrappedDek });
  }
  listSecrets(vaultId: string) {
    return this.request<{ vaultId: string; secrets: RemoteSecret[] }>("GET", `/vaults/${encodeURIComponent(vaultId)}/secrets`);
  }
  putSecrets(vaultId: string, upserts: Array<{ name: string; nonce: string; ciphertext: string; fingerprint: string }>, deletes: string[]) {
    return this.request<{ ok: boolean; applied: string[] }>("PUT", `/vaults/${encodeURIComponent(vaultId)}/secrets`, { upserts, deletes });
  }
  /**
   * Swap the vault to a fresh DEK. The whole next state goes over in one call
   * because the server applies it in a single transaction — see rekey.ts for
   * why a partial rotation is unrecoverable.
   */
  rekeyVault(
    vaultId: string,
    body: {
      grants: Array<{ email: string; wrappedDek: string }>;
      secrets: Array<{ name: string; nonce: string; ciphertext: string; fingerprint: string }>;
      revoke: string[];
    }
  ) {
    return this.request<RekeyResult>("POST", `/vaults/${encodeURIComponent(vaultId)}/rekey`, body);
  }
  listAudit(vaultId: string) {
    return this.request<{ audit: Array<Record<string, unknown>> }>("GET", `/vaults/${encodeURIComponent(vaultId)}/audit`);
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}
