// Typed mirrors of the GuruConnect server API responses.
// Shapes match server/src/api/*.rs exactly. Keep in sync with the Rust source
// of truth — these are hand-maintained, not generated.

// ---------------------------------------------------------------------------
// Auth
// ---------------------------------------------------------------------------

export type Role = "admin" | "operator" | "viewer";

export type Permission =
  | "view"
  | "control"
  | "transfer"
  | "manage_users"
  | "manage_clients";

/**
 * The canonical role set the server accepts (server/src/api/users.rs
 * `valid_roles`). The Users admin editor must offer exactly these — sending any
 * other value is a 400.
 */
export const ROLES: readonly Role[] = ["admin", "operator", "viewer"] as const;

/**
 * The canonical permission set the server accepts (server/src/api/users.rs
 * `valid_permissions`). These are the exact strings the rest of the app checks
 * (`view`/`control` gate viewer-token minting; `manage_users` gates the admin
 * plane). The permission editor must use these — an invented string is a 400.
 */
export const PERMISSIONS: readonly Permission[] = [
  "view",
  "control",
  "transfer",
  "manage_users",
  "manage_clients",
] as const;

/**
 * The server's role-default permissions (server/src/api/users.rs, the `match
 * request.role` block). When a user is created without an explicit permission
 * list the server seeds these. The create form mirrors them so the checkboxes
 * preview exactly what the server will store.
 */
export const ROLE_DEFAULT_PERMISSIONS: Record<Role, Permission[]> = {
  admin: ["view", "control", "transfer", "manage_users", "manage_clients"],
  operator: ["view", "control", "transfer"],
  viewer: ["view"],
};

export interface User {
  id: string;
  username: string;
  email: string | null;
  // role/permission come from the server as plain strings; widen defensively.
  role: Role | string;
  permissions: (Permission | string)[];
}

/**
 * Full admin-plane view of a user. Mirrors `api::users::UserInfo`
 * (server/src/api/users.rs) exactly — every field the list/create/get/update
 * endpoints return. The password hash is NEVER serialized by the server, so it
 * has no place in this type. `enabled` is the server's active/disabled flag
 * (a disabled user cannot log in). `email` and `last_login` are nullable.
 */
export interface UserAdmin {
  id: string;
  username: string;
  email: string | null;
  role: Role | string;
  enabled: boolean;
  created_at: string; // RFC3339
  last_login: string | null; // RFC3339
  permissions: (Permission | string)[];
}

/**
 * Body for `POST /api/users`. Mirrors `api::users::CreateUserRequest`.
 * `password` is required (server enforces >= 8 chars). `permissions` is
 * optional: when omitted the server seeds role-default permissions, so the
 * create UI sends it only when the admin overrides the defaults.
 */
export interface CreateUserRequest {
  username: string;
  password: string;
  email?: string | null;
  role: Role | string;
  permissions?: (Permission | string)[];
}

/**
 * Body for `PUT /api/users/:id`. Mirrors `api::users::UpdateUserRequest`.
 * `role` and `enabled` are required (the server always re-applies them).
 * `password`, when present, sets a new password (server enforces >= 8 chars);
 * omit it to leave the password unchanged. Permissions are NOT updated here —
 * they go through the dedicated permissions endpoint.
 */
export interface UpdateUserRequest {
  email?: string | null;
  role: Role | string;
  enabled: boolean;
  password?: string;
}

export interface LoginResponse {
  token: string;
  user: User;
}

export interface LoginRequest {
  username: string;
  password: string;
}

// ---------------------------------------------------------------------------
// Machines
// ---------------------------------------------------------------------------

export type MachineStatus = "online" | "offline";

export interface Machine {
  id: string;
  agent_id: string;
  hostname: string;
  os_version: string | null;
  is_elevated: boolean;
  is_persistent: boolean;
  first_seen: string; // RFC3339
  last_seen: string; // RFC3339
  status: MachineStatus | string;
}

export interface SessionRecord {
  id: string;
  started_at: string;
  ended_at: string | null;
  duration_secs: number | null;
  is_support_session: boolean;
  support_code: string | null;
  status: string;
}

export interface EventRecord {
  id: number;
  session_id: string;
  event_type: string;
  timestamp: string;
  viewer_id: string | null;
  viewer_name: string | null;
  details: unknown | null;
  ip_address: string | null;
}

export interface MachineHistory {
  machine: Machine;
  sessions: SessionRecord[];
  events: EventRecord[];
  exported_at: string;
}

export interface DeleteMachineParams {
  /** Send an uninstall command to the agent if it is online. */
  uninstall?: boolean;
  /** Include full history in the delete response before removal. */
  export?: boolean;
}

export interface DeleteMachineResponse {
  success: boolean;
  message: string;
  uninstall_sent: boolean;
  history: MachineHistory | null;
}

// ---------------------------------------------------------------------------
// Sessions (live relay state)
// ---------------------------------------------------------------------------

/**
 * Attended-consent state. Mirrors `connect_sessions.consent_state` and
 * `session::ConsentState::as_db_str`. Managed/persistent sessions are
 * `not_required`; attended (support-code) sessions move
 * `pending` -> `granted` | `denied`. A viewer may only join `not_required` or
 * `granted` (the relay refuses the others).
 */
export type ConsentState =
  | "not_required"
  | "pending"
  | "granted"
  | "denied";

/** A technician/viewer currently watching a session. Mirrors `ViewerInfoApi`. */
export interface SessionViewer {
  id: string;
  name: string;
  connected_at: string; // RFC3339
}

/**
 * Live session as returned by GET /api/sessions. Field names mirror
 * `api::SessionInfo` (server/src/api/mod.rs) exactly. This is in-memory relay
 * state, not the historical `connect_sessions` row (that is `SessionRecord`).
 */
export interface Session {
  id: string;
  agent_id: string;
  agent_name: string;
  started_at: string; // RFC3339
  viewer_count: number;
  viewers: SessionViewer[];
  is_streaming: boolean;
  is_online: boolean;
  is_persistent: boolean;
  last_heartbeat: string; // RFC3339
  os_version: string | null;
  is_elevated: boolean;
  uptime_secs: number;
  display_count: number;
  agent_version: string | null;
  consent_state: ConsentState | string;
}

/** Access mode the relay grants a minted viewer token. */
export type ViewerAccess = "control" | "view_only";

/**
 * Response from POST /api/sessions/:id/viewer-token. Mirrors
 * `api::sessions::ViewerTokenResponse`. The signed token carries the
 * authoritative access claim; `access` here is the echoed mode.
 */
export interface ViewerTokenResponse {
  token: string;
  session_id: string;
  expires_in_secs: number;
  access: ViewerAccess | string;
}

// ---------------------------------------------------------------------------
// Support codes (attended-support, one-time)
// ---------------------------------------------------------------------------

/**
 * Lifecycle state of a support code. Mirrors `support_codes::CodeStatus`
 * (`#[serde(rename_all = "lowercase")]`), serialized as the `status` field:
 *   pending   — generated, waiting for an end user to redeem it (single-use).
 *   connected — redeemed; an attended session is now bound to it.
 *   completed — that session ended normally.
 *   cancelled — revoked by a tech before it was redeemed.
 * `GET /api/codes` returns only `pending` and `connected` (the active set);
 * `completed`/`cancelled` are modeled for completeness and defensive rendering.
 */
export type CodeStatus =
  | "pending"
  | "connected"
  | "completed"
  | "cancelled";

/**
 * A support code as returned by `POST /api/codes` and `GET /api/codes`. Field
 * names mirror `support_codes::SupportCode` (serde default snake_case) exactly.
 *
 * NOTE: the in-memory `SupportCode` the API serializes has NO `expires_at`
 * field (only the durable DB row does); codes are short-lived and the dashboard
 * surfaces liveness via the poll + status, not an absolute expiry. `code` is the
 * grouped `XXX-XXX-XXX` value the tech reads to the end user.
 */
export interface SupportCode {
  code: string;
  session_id: string; // UUID
  created_by: string;
  created_at: string; // RFC3339
  status: CodeStatus | string;
  client_name: string | null;
  client_machine: string | null;
  connected_at: string | null; // RFC3339, set when redeemed
}

/**
 * Body for `POST /api/codes`. Mirrors `support_codes::CreateCodeRequest`. Both
 * fields are optional; the server stamps `created_by` from `technician_name`
 * (falling back to "Unknown"). `technician_id` is accepted but currently unused
 * server-side. We send `technician_name` so the code is attributed to the
 * signed-in operator.
 */
export interface CreateCodeRequest {
  technician_id?: string;
  technician_name?: string;
}

// ---------------------------------------------------------------------------
// Per-agent keys (admin plane)
// ---------------------------------------------------------------------------

export interface KeyMetadata {
  id: string;
  machine_id: string;
  created_at: string;
  last_used_at: string | null;
  revoked_at: string | null;
}

/** Returned exactly once when a key is minted. `key` is plaintext `cak_...`. */
export interface CreatedKey {
  id: string;
  machine_id: string;
  key: string;
  created_at: string;
}
