export type ServerStatus = "pending" | "active" | "offline";
export type KeySource = "uploaded" | "generated";

export interface PackageUpdate {
    name: string;
    current_version?: string;
    new_version: string;
}

export interface Inventory {
    cpu: { model?: string; cores?: number; usage_pct: number; load1?: number };
    memory: { total_bytes: number; used_bytes: number };
    swap_total_bytes: number;
    swap_used_bytes: number;
    partitions?: { device: string; mountpoint: string; fstype?: string; total_bytes: number; used_bytes: number }[];
    kernel?: string;
    metrics_at?: string;
    static_at?: string;
}

export interface Server {
    id: string;
    server_id: string;
    hostname: string;
    ip_address: string;
    os_info: string;
    status: ServerStatus;
    agent_version?: string;
    last_seen: string;
    created_at: string;
    available_updates?: PackageUpdate[];
    updates_checked_at?: string;
    console_protocols?: string[];
    inventory?: Inventory;
    tags?: Record<string, string>;
}

export type MonitorType = "http" | "tcp" | "icmp" | "tls";
export type MonitorStatus = "up" | "down" | "pending";

export interface MonitorTarget {
    url?: string;
    host?: string;
    port?: number;
    method?: string;
    expected_status?: number;
    keyword?: string;
    tls_warn_days?: number;
    insecure?: boolean;
}

export interface MonitorState {
    status: MonitorStatus;
    last_check_at?: string;
    latency_ms: number;
    message?: string;
    cert_expiry_at?: string;
    fails: number;
}

export interface Monitor {
    monitor_id: string;
    name: string;
    type: MonitorType;
    target: MonitorTarget;
    interval_sec: number;
    runner: string; // "server" or a server_id
    retries: number;
    enabled: boolean;
    channel_ids?: string[];
    state: MonitorState;
    created_at: string;
}

export interface MonitorInput {
    name: string;
    type: MonitorType;
    target: MonitorTarget;
    interval_sec: number;
    runner: string;
    retries: number;
    enabled: boolean;
    channel_ids?: string[];
}

export interface Incident {
    incident_id: string;
    monitor_id: string;
    started_at: string;
    resolved_at?: string;
    cause?: string;
}

export interface Rollup {
    monitor_id: string;
    period_start: string;
    checks: number;
    up_count: number;
    sum_latency: number;
}

export type ChannelType = "webhook" | "smtp" | "discord" | "slack" | "telegram";

export interface NotificationChannel {
    channel_id: string;
    name: string;
    type: ChannelType;
    config: Record<string, string>;
    enabled: boolean;
    created_at: string;
}

export interface ChannelInput {
    name: string;
    type: ChannelType;
    config: Record<string, string>;
    enabled: boolean;
}

export interface ConsoleConnectRequest {
    server_id: string;
    protocol: string;
    key_id?: string;
    rdp_username?: string;
    rdp_password?: string;
    ssh_username?: string;
}

export interface ConsoleConnectResponse {
    session_id: string;
    token: string;
    ws_path: string;
}

export interface Key {
    id: string;
    key_id: string;
    label: string;
    public_key: string;
    fingerprint: string;
    source: KeySource;
    generated_by_server_id?: string;
    has_private_key: boolean;
    has_passphrase?: boolean;
    created_at: string;
    assigned_count?: number;
}

export interface Assignment {
    id: string;
    key_id: string;
    server_id: string;
    assigned_at: string;
    revoked_at: string | null;
}

export interface AuditEvent {
    id: string;
    event_type: string;
    actor: string;
    server_id?: string;
    key_id?: string;
    details: string;
    created_at: string;
}

export interface AlertSettings {
    offline_threshold_minutes: number;
    offline_channel_ids: string[] | null;
}

export interface SecretsSettings {
    read_token_set: boolean;
    rotated_at?: string;
}

export interface Settings {
    alerts: AlertSettings;
    secrets: SecretsSettings;
    workflow_log_retention_days?: number | null;
    local_login_enabled?: boolean;
}

export interface SecretGroupSummary {
    group: string;
    key_count: number;
    updated_at: string;
}

export interface Secret {
    group: string;
    key: string;
    updated_at: string;
}

export interface NewServerResponse {
    server_id: string;
    pre_reg_token: string;
    install_command: string;
    install_command_ps: string;
}

export interface GenerateKeyOptions {
    label: string;
    key_type: "ed25519" | "rsa" | "ecdsa";
    key_size?: number;
    passphrase?: string;
    comment?: string;
}

export interface KeyWithAssignments extends Key {
    assignments: (Assignment & { server: Server })[];
}

export interface ServerWithKeys extends Server {
    keys: (Assignment & { key: Key })[];
}

export interface InputParam {
    name: string;
    default: string;
    description: string;
}

export interface WorkflowStep {
    step_id: string;
    name: string;
    description: string;
    interpreter: "bash" | "powershell";
    script: string;
    declared_outputs: string[];
    declared_inputs: InputParam[];
    secret_refs: string[];
    source?: "user" | "default";
    slug?: string;
}

export interface WorkflowStepRef {
    step_id?: string;
    inline?: WorkflowStep;
    order: number;
    on_failure: "stop" | "continue" | "retry";
    max_retries: number;
    overrides?: { script?: string; secret_refs?: string[] };
    inputs?: Record<string, string>;
}

export interface Workflow {
    workflow_id: string;
    name: string;
    target_server_ids: string[];
    target_tags?: Record<string, string>;
    steps: WorkflowStepRef[];
    schedule?: Schedule;
    next_run_at?: string;
    last_run_at?: string;
    last_skipped?: Skip;
}

export interface StepRun {
    order: number;
    name: string;
    status: string;
    attempts: number;
    exit_code: number;
    log_offset: number;
    output_env: Record<string, string>;
    started_at?: string;
    finished_at?: string;
}

export interface ServerRun {
    server_id: string;
    hostname: string;
    status: string;
    run_env: Record<string, string>;
    steps: StepRun[];
    started_at?: string;
    finished_at?: string;
}

export interface WorkflowRun {
    run_id: string;
    workflow_id: string;
    name: string;
    status: string;
    triggered_by: string;
    started_at: string;
    finished_at?: string;
    server_runs: ServerRun[];
}

export type Role = "owner" | "admin" | "member";

/** The session as returned by GET /auth/me mirrors auth.Session on the server. */
export interface SessionUser {
    user_id: string;
    instance_id: string;
    role: Role;
    email: string;
    name: string;
}

export interface Instance {
    instance_id: string;
    name: string;
    slug: string;
    created_at: string;
}

export interface MeResponse {
    user: SessionUser;
    instance: Instance | null;
}

export interface BootstrapStatus {
    needs_setup: boolean;
    instance_name?: string;
}

export interface BootstrapResponse {
    instance: Instance;
    slug: string;
    instance_id: string;
}

export interface InstanceUser {
    user_id: string;
    instance_id: string;
    email: string;
    role: Role;
    // "hq" means the row was projected from a Vantage HQ account. Its role,
    // password and existence belong to HQ; this instance refuses to change them.
    auth_source: "local" | "oidc" | "hq";
    hq_user_id?: string;
    created_at: string;
    last_login?: string;
}

export interface OrgUserInput {
    email: string;
    password: string;
    role: Role;
}

export interface PublicProvider {
    id: string;
    name: string;
    preset: string;
}

export interface ProvidersResponse {
    local_enabled: boolean;
    providers: PublicProvider[];
}

export interface AuthProvider {
    provider_id: string;
    instance_id: string;
    name: string;
    kind: "oidc" | "oauth2";
    preset: string;
    issuer: string;
    client_id: string;
    scopes: string[];
    enabled: boolean;
    callback_notice: boolean;
    order: number;
    created_at: string;
    updated_at: string;
    client_secret_set: boolean;
    callback_url: string;
}

export interface AuthPreset {
    id: string;
    label: string;
    kind: "oidc" | "oauth2";
    input_label: string;
    input_hint: string;
}

export interface AuthProviderInput {
    name: string;
    preset: string;
    issuer_input?: string;
    client_id: string;
    client_secret: string;
    enabled: boolean;
}

export interface AuthProviderUpdate {
    name?: string;
    issuer_input?: string;
    client_id?: string;
    client_secret?: string;
    enabled?: boolean;
    order?: number;
}

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

async function request<T>(path: string, options?: RequestInit): Promise<T> {
    const res = await fetch(`/api${path}`, {
        credentials: "include",
        headers: {
            "Content-Type": "application/json",
            ...options?.headers,
        },
        ...options,
    });

    if (!res.ok) {
        const text = await res.text().catch(() => "");

        let message = text || res.statusText || `HTTP ${res.status}`;
        try {
            const body = JSON.parse(text);
            if (body?.error) message = body.error;
        } catch {}
        throw new ApiError(res.status, message);
    }

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

    return res.json();
}

async function authRequest<T>(path: string, options?: RequestInit): Promise<T> {
    const res = await fetch(path, {
        credentials: "include",
        headers: { "Content-Type": "application/json", ...options?.headers },
        ...options,
    });

    if (!res.ok) {
        let message = `HTTP ${res.status}`;
        try {
            const body = await res.json();
            if (body?.error) message = body.error;
        } catch {}
        throw new ApiError(res.status, message);
    }

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

    return res.json();
}

export const auth = {
    bootstrapStatus(): Promise<BootstrapStatus> {
        return authRequest<BootstrapStatus>("/auth/bootstrap-status");
    },

    bootstrap(input: { instance_name: string; email: string; password: string }): Promise<BootstrapResponse> {
        return authRequest<BootstrapResponse>("/auth/bootstrap", {
            method: "POST",
            body: JSON.stringify(input),
        });
    },

    login(email: string, password: string): Promise<{ ok: boolean }> {
        return authRequest<{ ok: boolean }>("/auth/login", {
            method: "POST",
            body: JSON.stringify({ email, password }),
        });
    },

    logout(): Promise<void> {
        return authRequest<void>("/auth/logout", { method: "POST" });
    },

    me(): Promise<MeResponse> {
        return authRequest<MeResponse>("/auth/me");
    },

    /** Unauthenticated: what the login page draws itself from. */
    providers(): Promise<ProvidersResponse> {
        return authRequest<ProvidersResponse>("/auth/providers");
    },

    /** Where a provider button sends the browser. */
    ssoStartUrl(providerId: string): string {
        return `/auth/oidc/${providerId}/start`;
    },
};

export const api = {
    listInstanceUsers(): Promise<InstanceUser[]> {
        return request<InstanceUser[]>("/instance/users");
    },

    createInstanceUser(input: OrgUserInput): Promise<InstanceUser> {
        return request<InstanceUser>("/instance/users", { method: "POST", body: JSON.stringify(input) });
    },

    updateInstanceUserRole(userId: string, role: Role): Promise<{ ok: boolean }> {
        return request<{ ok: boolean }>(`/instance/users/${userId}/role`, {
            method: "PUT",
            body: JSON.stringify({ role }),
        });
    },

    deleteInstanceUser(userId: string): Promise<void> {
        return request<void>(`/instance/users/${userId}`, { method: "DELETE" });
    },

    listAuthPresets(): Promise<AuthPreset[]> {
        return request<AuthPreset[]>("/auth/presets");
    },

    listAuthProviders(): Promise<AuthProvider[]> {
        return request<AuthProvider[]>("/auth/providers");
    },

    createAuthProvider(input: AuthProviderInput): Promise<AuthProvider> {
        return request<AuthProvider>("/auth/providers", { method: "POST", body: JSON.stringify(input) });
    },

    updateAuthProvider(id: string, input: AuthProviderUpdate): Promise<{ saved: boolean }> {
        return request<{ saved: boolean }>(`/auth/providers/${id}`, { method: "PUT", body: JSON.stringify(input) });
    },

    deleteAuthProvider(id: string): Promise<{ deleted: boolean }> {
        return request<{ deleted: boolean }>(`/auth/providers/${id}`, { method: "DELETE" });
    },

    testAuthProvider(id: string): Promise<{ ok: boolean; message: string }> {
        return request<{ ok: boolean; message: string }>(`/auth/providers/${id}/test`, { method: "POST" });
    },

    ackAuthProviderNotice(id: string): Promise<{ acknowledged: boolean }> {
        return request<{ acknowledged: boolean }>(`/auth/providers/${id}/ack-notice`, { method: "POST" });
    },

    listServers(tags?: Record<string, string>): Promise<Server[]> {
        const params = Object.entries(tags ?? {}).map(([k, v]) => `tag=${encodeURIComponent(`${k}:${v}`)}`);
        return request<Server[]>(`/servers${params.length ? `?${params.join("&")}` : ""}`);
    },

    listKnownTags(): Promise<Record<string, string[]>> {
        return request<Record<string, string[]>>("/servers/tags");
    },

    setServerTags(serverId: string, tags: Record<string, string>): Promise<{ tags: Record<string, string> }> {
        return request<{ tags: Record<string, string> }>(`/servers/${serverId}/tags`, {
            method: "PUT",
            body: JSON.stringify({ tags }),
        });
    },

    getServer(serverId: string): Promise<ServerWithKeys> {
        return request<ServerWithKeys>(`/servers/${serverId}`);
    },

    createServer(): Promise<NewServerResponse> {
        return request<NewServerResponse>("/servers/new", { method: "POST" });
    },

    deleteServer(serverId: string): Promise<void> {
        return request<void>(`/servers/${serverId}`, { method: "DELETE" });
    },

    generateKeyForServer(serverId: string, opts: GenerateKeyOptions): Promise<{ command_id: string }> {
        return request<{ command_id: string }>(`/servers/${serverId}/generate-key`, {
            method: "POST",
            body: JSON.stringify(opts),
        });
    },

    getUpdateCommand(osInfo?: string): string {
        if (osInfo && osInfo.toLowerCase().includes("windows")) {
            return `irm "${window.location.origin}/update.ps1" | iex`;
        }
        return `curl -fsSL "${window.location.origin}/update" | bash`;
    },

    listMonitors(): Promise<Monitor[]> {
        return request<Monitor[]>("/monitors");
    },

    getMonitor(monitorId: string): Promise<Monitor> {
        return request<Monitor>(`/monitors/${monitorId}`);
    },

    createMonitor(input: MonitorInput): Promise<Monitor> {
        return request<Monitor>("/monitors", { method: "POST", body: JSON.stringify(input) });
    },

    updateMonitor(monitorId: string, input: Partial<MonitorInput>): Promise<void> {
        return request<void>(`/monitors/${monitorId}`, { method: "PUT", body: JSON.stringify(input) });
    },

    deleteMonitor(monitorId: string): Promise<void> {
        return request<void>(`/monitors/${monitorId}`, { method: "DELETE" });
    },

    getMonitorIncidents(monitorId: string): Promise<Incident[]> {
        return request<Incident[]>(`/monitors/${monitorId}/incidents`);
    },

    getMonitorUptime(monitorId: string): Promise<Rollup[]> {
        return request<Rollup[]>(`/monitors/${monitorId}/uptime`);
    },

    listChannels(): Promise<NotificationChannel[]> {
        return request<NotificationChannel[]>("/channels");
    },

    createChannel(input: ChannelInput): Promise<NotificationChannel> {
        return request<NotificationChannel>("/channels", { method: "POST", body: JSON.stringify(input) });
    },

    updateChannel(channelId: string, input: Partial<ChannelInput>): Promise<void> {
        return request<void>(`/channels/${channelId}`, { method: "PUT", body: JSON.stringify(input) });
    },

    deleteChannel(channelId: string): Promise<void> {
        return request<void>(`/channels/${channelId}`, { method: "DELETE" });
    },

    testChannel(channelId: string): Promise<{ status: string }> {
        return request<{ status: string }>(`/channels/${channelId}/test`, { method: "POST" });
    },

    getLatestAgentVersion(): Promise<{ version: string }> {
        return request<{ version: string }>("/agent/latest-version");
    },

    updateAgent(serverId: string): Promise<{ message: string; version: string }> {
        return request<{ message: string; version: string }>(`/servers/${serverId}/update-agent`, {
            method: "POST",
        });
    },

    applyUpdates(serverId: string): Promise<{ message: string }> {
        return request<{ message: string }>(`/servers/${serverId}/apply-updates`, {
            method: "POST",
        });
    },

    listAuditEvents(limit?: number): Promise<AuditEvent[]> {
        const qs = limit ? `?limit=${limit}` : "";
        return request<AuditEvent[]>(`/audit${qs}`);
    },

    getSettings(): Promise<Settings> {
        return request<Settings>("/settings");
    },

    saveSettings(settings: { alerts: AlertSettings; workflow_log_retention_days?: number | null; local_login_enabled?: boolean }): Promise<{ saved: boolean }> {
        return request<{ saved: boolean }>("/settings", {
            method: "PUT",
            body: JSON.stringify(settings),
        });
    },

    rotateSecretsToken(): Promise<{ token: string }> {
        return request<{ token: string }>("/settings/secrets-token", { method: "POST" });
    },

    listSecretGroups(): Promise<SecretGroupSummary[]> {
        return request<SecretGroupSummary[]>("/secrets");
    },

    createSecretGroup(group: string, values: Record<string, string>): Promise<{ group: string }> {
        return request<{ group: string }>("/secrets", {
            method: "POST",
            body: JSON.stringify({ group, values }),
        });
    },

    getSecretGroup(group: string): Promise<{ group: string; secrets: Secret[] }> {
        return request<{ group: string; secrets: Secret[] }>(`/secrets/${encodeURIComponent(group)}`);
    },

    putSecrets(group: string, values: Record<string, string>): Promise<{ saved: boolean }> {
        return request<{ saved: boolean }>(`/secrets/${encodeURIComponent(group)}`, {
            method: "PUT",
            body: JSON.stringify(values),
        });
    },

    revealSecret(group: string, key: string): Promise<{ value: string }> {
        return request<{ value: string }>(`/secrets/${encodeURIComponent(group)}/reveal`, {
            method: "POST",
            body: JSON.stringify({ key }),
        });
    },

    deleteSecret(group: string, key: string): Promise<void> {
        return request<void>(`/secrets/${encodeURIComponent(group)}/${encodeURIComponent(key)}`, {
            method: "DELETE",
        });
    },

    deleteSecretGroup(group: string): Promise<void> {
        return request<void>(`/secrets/${encodeURIComponent(group)}`, { method: "DELETE" });
    },

    listKeys(): Promise<Key[]> {
        return request<Key[]>("/keys");
    },

    getKey(keyId: string): Promise<KeyWithAssignments> {
        return request<KeyWithAssignments>(`/keys/${keyId}`);
    },

    uploadKey(label: string, public_key: string, private_key?: string, passphrase?: string): Promise<Key> {
        return request<Key>("/keys", {
            method: "POST",
            body: JSON.stringify({
                label,
                public_key,
                private_key: private_key || undefined,
                passphrase: passphrase || undefined,
            }),
        });
    },

    getPrivateKey(keyId: string): Promise<{ private_key: string }> {
        return request<{ private_key: string }>(`/keys/${keyId}/private-key`);
    },

    deleteKey(keyId: string): Promise<void> {
        return request<void>(`/keys/${keyId}`, { method: "DELETE" });
    },

    assignKey(keyId: string, serverId: string): Promise<Assignment> {
        return request<Assignment>(`/keys/${keyId}/assign`, {
            method: "POST",
            body: JSON.stringify({ server_id: serverId }),
        });
    },

    revokeKey(keyId: string, serverId: string): Promise<void> {
        return request<void>(`/keys/${keyId}/assign/${serverId}`, {
            method: "DELETE",
        });
    },

    connectConsole(body: ConsoleConnectRequest): Promise<ConsoleConnectResponse> {
        return request<ConsoleConnectResponse>("/console/connect", {
            method: "POST",
            body: JSON.stringify(body),
        });
    },

    listSteps(): Promise<WorkflowStep[]> {
        return request<WorkflowStep[]>("/steps");
    },

    createStep(s: Partial<WorkflowStep>): Promise<WorkflowStep> {
        return request<WorkflowStep>("/steps", {
            method: "POST",
            body: JSON.stringify(s),
        });
    },

    updateStep(stepId: string, s: Partial<WorkflowStep>): Promise<WorkflowStep> {
        return request<WorkflowStep>(`/steps/${stepId}`, {
            method: "PUT",
            body: JSON.stringify(s),
        });
    },

    deleteStep(stepId: string): Promise<void> {
        return request<void>(`/steps/${stepId}`, { method: "DELETE" });
    },

    exportStepUrl(stepId: string): string {
        return `/api/steps/${stepId}/export`;
    },

    importStep(doc: unknown): Promise<WorkflowStep> {
        return request<WorkflowStep>("/steps/import", {
            method: "POST",
            body: JSON.stringify(doc),
        });
    },

    parseStep(doc: unknown): Promise<WorkflowStep> {
        return request<WorkflowStep>("/steps/parse", {
            method: "POST",
            body: JSON.stringify(doc),
        });
    },

    seedDefaults(): Promise<{ created: number; updated: number }> {
        return request<{ created: number; updated: number }>("/steps/seed-defaults", {
            method: "POST",
        });
    },

    stepUsage(): Promise<Record<string, number>> {
        return request<Record<string, number>>("/steps/usage");
    },

    listWorkflows(): Promise<Workflow[]> {
        return request<Workflow[]>("/workflows");
    },

    getWorkflow(workflowId: string): Promise<Workflow> {
        return request<Workflow>(`/workflows/${workflowId}`);
    },

    createWorkflow(w: Partial<Workflow>): Promise<Workflow> {
        return request<Workflow>("/workflows", {
            method: "POST",
            body: JSON.stringify(w),
        });
    },

    updateWorkflow(workflowId: string, w: Partial<Workflow>): Promise<Workflow> {
        return request<Workflow>(`/workflows/${workflowId}`, {
            method: "PUT",
            body: JSON.stringify(w),
        });
    },

    deleteWorkflow(workflowId: string): Promise<void> {
        return request<void>(`/workflows/${workflowId}`, { method: "DELETE" });
    },

    runWorkflow(workflowId: string): Promise<{ run_id: string }> {
        return request<{ run_id: string }>(`/workflows/${workflowId}/run`, {
            method: "POST",
        });
    },

    listRuns(workflowId: string): Promise<WorkflowRun[]> {
        return request<WorkflowRun[]>(`/workflows/${workflowId}/runs`);
    },

    getRun(runId: string): Promise<WorkflowRun> {
        return request<WorkflowRun>(`/runs/${runId}`);
    },

    cancelRun(runId: string): Promise<void> {
        return request<void>(`/runs/${runId}/cancel`, { method: "POST" });
    },

    async getServerRunLog(runId: string, serverId: string): Promise<string> {
        const res = await fetch(`/api/runs/${runId}/servers/${serverId}/logs`, {
            credentials: "include",
        });
        if (!res.ok) throw new Error("no logs");
        return res.text();
    },

    serverRunLogStreamUrl(runId: string, serverId: string): string {
        return `/api/runs/${runId}/servers/${serverId}/logs/stream`;
    },

    setWorkflowSchedule(workflowId: string, schedule: Schedule): Promise<{ schedule: Schedule; next_run_at: string | null }> {
        return request(`/workflows/${workflowId}/schedule`, { method: "PUT", body: JSON.stringify(schedule) });
    },

    previewSchedule(workflowId: string, cron: string, tz: string): Promise<{ occurrences: string[] }> {
        return request(`/workflows/${workflowId}/schedule/preview?cron=${encodeURIComponent(cron)}&tz=${encodeURIComponent(tz)}`);
    },
};

export type LicenseState = "valid" | "expired" | "invalid";

export interface LicenseInfo {
    instance_id: string;
    state: LicenseState;
    reason?: string;
    tier?: string;
    support_level?: string;
    expires_at?: string;
    days_remaining?: number;
    limits: {
        max_servers: number;
        max_monitors: number;
        max_secret_groups: number;
        max_channels: number;
        audit_retention_days: number;
    };
    features: Record<string, boolean>;
    usage: {
        servers: number;
        monitors: number;
        secret_groups: number;
        channels: number;
    };
    source: string;
    /** "cloud" | "self_hosted". A cloud instance's licence is managed in HQ. */
    deployment: string;
}

export type Severity = "critical" | "high" | "medium" | "low" | "unknown";
export type FindingState = "open" | "fixed" | "accepted";

export interface Acceptance {
    by: string;
    reason: string;
    until: string;
    at: string;
}

export interface VulnFinding {
    id: string;
    server_id: string;
    cve_id: string;
    package_name: string;
    installed_version: string;
    /** Absent means no vendor fix is published — a real state, not missing data. */
    fixed_in?: string;
    severity: Severity;
    cvss_score?: number;
    title?: string;
    references?: string[];
    state: FindingState;
    first_seen: string;
    last_seen: string;
    fixed_at?: string;
    accepted?: Acceptance;
}

/** One CVE across every server it affects. The board groups by CVE because the
 *  same CVE on forty servers is one decision, not forty rows. */
export interface VulnGroup {
    cve_id: string;
    severity: Severity;
    title?: string;
    server_count: number;
    findings: VulnFinding[];
}

export interface VulnSummary {
    counts: Partial<Record<Severity, number>>;
    db_version?: number;
    pulled_at?: string;
    last_full_scan_at?: string;
    last_error?: string;
}

export interface InstalledPackage {
    name: string;
    version: string;
    epoch?: number;
    arch: string;
    source_name?: string;
}

export interface ServerPackages {
    server_id: string;
    os: { family: string; version_id: string; arch: string };
    hash: string;
    packages: InstalledPackage[];
    collected_at: string;
    scan_pending: boolean;
    scanned_at?: string;
    /** "ok" | "unsupported". Unsupported must never read as "clean". */
    status: string;
    db_version: number;
}

export interface PackageHit {
    server_id: string;
    name: string;
    version: string;
}

export interface VulnAlertRule {
    id: string;
    name: string;
    enabled: boolean;
    min_severity: Severity;
    tags?: Record<string, string>;
    channel_ids: string[];
    created_at: string;
    updated_at: string;
}

export interface VulnAlertRuleInput {
    name: string;
    enabled: boolean;
    min_severity: Severity;
    tags?: Record<string, string>;
    channel_ids: string[];
}

export const vulnerabilities = {
    list(params?: { severity?: string; state?: string; server?: string; hasFix?: boolean; tags?: Record<string, string> }): Promise<VulnGroup[]> {
        const q = new URLSearchParams();
        if (params?.severity) q.set("severity", params.severity);
        if (params?.state) q.set("state", params.state);
        if (params?.server) q.set("server", params.server);
        // Explicitly undefined-checked: `false` is a real selection here (the
        // unfixable set), so a truthiness test would silently drop it.
        if (params?.hasFix !== undefined) q.set("has_fix", String(params.hasFix));
        for (const [k, v] of Object.entries(params?.tags ?? {})) q.append("tag", `${k}:${v}`);
        const qs = q.toString();
        return request<VulnGroup[]>(`/vulnerabilities${qs ? `?${qs}` : ""}`);
    },

    summary(): Promise<VulnSummary> {
        return request<VulnSummary>("/vulnerabilities/summary");
    },

    rescan(): Promise<{ queued: number }> {
        return request<{ queued: number }>("/vulnerabilities/rescan", { method: "POST" });
    },

    accept(id: string, reason: string, until: string): Promise<VulnFinding> {
        return request<VulnFinding>(`/vulnerabilities/${id}/accept`, {
            method: "POST",
            body: JSON.stringify({ reason, until }),
        });
    },

    unaccept(id: string): Promise<VulnFinding> {
        return request<VulnFinding>(`/vulnerabilities/${id}/accept`, { method: "DELETE" });
    },

    forServer(serverId: string): Promise<VulnFinding[]> {
        return request<VulnFinding[]>(`/servers/${serverId}/vulnerabilities`);
    },

    packagesForServer(serverId: string): Promise<ServerPackages | { reported: false }> {
        return request<ServerPackages | { reported: false }>(`/servers/${serverId}/packages`);
    },

    searchPackages(name: string): Promise<PackageHit[]> {
        return request<PackageHit[]>(`/packages/search?name=${encodeURIComponent(name)}`);
    },

    listRules(): Promise<VulnAlertRule[]> {
        return request<VulnAlertRule[]>("/vuln-rules");
    },

    createRule(input: VulnAlertRuleInput): Promise<VulnAlertRule> {
        return request<VulnAlertRule>("/vuln-rules", { method: "POST", body: JSON.stringify(input) });
    },

    updateRule(id: string, input: VulnAlertRuleInput): Promise<{ status: string }> {
        return request<{ status: string }>(`/vuln-rules/${id}`, { method: "PUT", body: JSON.stringify(input) });
    },

    deleteRule(id: string): Promise<{ status: string }> {
        return request<{ status: string }>(`/vuln-rules/${id}`, { method: "DELETE" });
    },
};

export type WorkloadKind = "container" | "unit";
export type WorkloadAction = "start" | "stop" | "restart";

/** One container or one systemd unit.
 *
 *  `state` is deliberately not a shared vocabulary across the two kinds:
 *  containers report running/exited/paused/restarting/created, units report
 *  active/inactive/failed/activating. A failed unit and an exited container
 *  mean different things. */
export interface Workload {
    kind: WorkloadKind;
    id: string;
    name: string;
    state: string;
    health?: string;
    image?: string;
    stack?: string;
    ports?: string[];
    restarts?: number;
    started_at?: string;
    protected: boolean;
}

export interface ServerWorkloads {
    server_id: string;
    hash?: string;
    workloads: Workload[];
    collected_at?: string;
    /** false with no error means "Docker not in use here", which is not a
     *  fault. With an error it means installed but not responding. */
    docker_ok: boolean;
    docker_error?: string;
    systemd_ok: boolean;
    systemd_error?: string;
}

export interface WorkloadHit {
    server_id: string;
    workload: Workload;
}

export const workloads = {
    forServer(serverId: string): Promise<ServerWorkloads> {
        return request<ServerWorkloads>(`/servers/${serverId}/workloads`);
    },

    refresh(serverId: string): Promise<{ message: string }> {
        return request<{ message: string }>(`/servers/${serverId}/workloads/refresh`, { method: "POST" });
    },

    control(serverId: string, kind: WorkloadKind, id: string, action: WorkloadAction): Promise<{ message: string }> {
        return request<{ message: string }>(
            `/servers/${serverId}/workloads/${encodeURIComponent(id)}/action`,
            { method: "POST", body: JSON.stringify({ kind, action }) },
        );
    },

    logs(serverId: string, kind: WorkloadKind, id: string, tail = 500): Promise<{ text: string; truncated: boolean }> {
        return request<{ text: string; truncated: boolean }>(
            `/servers/${serverId}/workloads/${encodeURIComponent(id)}/logs?kind=${kind}&tail=${tail}`,
        );
    },

    search(params?: { image?: string; stack?: string; state?: string }): Promise<WorkloadHit[]> {
        const q = new URLSearchParams();
        if (params?.image) q.set("image", params.image);
        if (params?.stack) q.set("stack", params.stack);
        if (params?.state) q.set("state", params.state);
        const qs = q.toString();
        return request<WorkloadHit[]>(`/workloads${qs ? `?${qs}` : ""}`);
    },
};

// `request` already prefixes /api, so these paths do not repeat it.
export const licence = {
    get(): Promise<LicenseInfo> {
        return request<LicenseInfo>("/license");
    },
    put(blob: string): Promise<{ state: LicenseState; tier: string; expires_at?: string }> {
        return request("/license", { method: "POST", body: JSON.stringify({ blob }) });
    },
};

export interface Schedule {
    enabled: boolean;
    cron: string;
    tz: string;
}

export interface Skip {
    reason: string;
    due: string;
    at: string;
}
