/*
 * The typed client for the licensing service.
 *
 * The browser calls admin directly, so every request carries credentials and
 * every failure mode is one of three: the API is unreachable (NotConnected),
 * the caller is not signed in (ApiError 401, which layouts redirect on), or the
 * request was refused (ApiError with the backend's own message, which is
 * customer-facing and should be shown verbatim).
 */

export const API_BASE = (process.env.NEXT_PUBLIC_ADMIN_API_URL ?? "").replace(/\/$/, "");

export class NotConnected extends Error {
    constructor() {
        super("not connected");
        this.name = "NotConnected";
    }
}

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

async function req<T>(path: string, init?: RequestInit): Promise<T> {
    if (!API_BASE) throw new NotConnected();

    let res: Response;
    try {
        res = await fetch(`${API_BASE}${path}`, {
            ...init,
            credentials: "include",
            headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
        });
    } catch {
        // Network-level failure, DNS, or a CORS preflight the browser refused.
        throw new NotConnected();
    }

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

    const body = await res.json().catch(() => null);
    if (!res.ok) {
        throw new ApiError(res.status, body?.error ?? `request failed (${res.status})`);
    }
    return body as T;
}

const post = <T,>(path: string, payload?: unknown) =>
    req<T>(path, { method: "POST", body: payload ? JSON.stringify(payload) : undefined });

const put = <T,>(path: string, payload?: unknown) =>
    req<T>(path, { method: "PUT", body: payload ? JSON.stringify(payload) : undefined });

const del = <T,>(path: string) => req<T>(path, { method: "DELETE" });

// --- types ---------------------------------------------------------------

export type Deployment = "cloud" | "self_hosted";
export type Tier = "free" | "professional" | "enterprise";
export type Term = "monthly" | "annual";
export type InstanceStatus = "awaiting_link" | "active" | "lapsed" | "cancelled" | "deleted";

/*
 * Two role vocabularies, same three words. AccountRole governs the HQ account:
 * who may invite, create instances and grant access. InstanceRole is the role a
 * projected user holds INSIDE one instance. A person can be an account member
 * and an instance owner at once — that is normal, not a mistake.
 */
export type AccountRole = "owner" | "admin" | "member";
export type InstanceRole = "owner" | "admin" | "member";

export interface Session {
    kind: "staff" | "customer";
    email: string;
    account_id?: string;
    account_role?: AccountRole;
}

export interface AccountUser {
    user_id: string;
    account_id: string;
    email: string;
    account_role: AccountRole;
    verified_at?: string | null;
    hq_sync_failed_at?: string | null;
    created_at: string;
}

export interface InstanceMember {
    member_id: string;
    account_id: string;
    instance_id: string;
    customer_user_id: string;
    control_user_id: string;
    role: InstanceRole;
    email: string;
    created_at: string;
}

export interface Limits {
    max_servers: number;
    max_monitors: number;
    max_secret_groups: number;
    max_channels: number;
    audit_retention_days: number;
}

export interface Account {
    account_id: string;
    name: string;
    billing_email: string;
    paddle_customer_id?: string;
    status: "active" | "suspended";
    created_at: string;
}

export interface Instance {
    instance_id: string;
    account_id: string;
    name: string;
    slug?: string;
    deployment: Deployment;
    tier?: Tier;
    status: InstanceStatus;
    current_license?: string;
    relink_count: number;
    inject_failed_at?: string | null;
    notices_sent?: string[];
    created_at: string;
}

export interface License {
    license_id: string;
    instance_id: string;
    account_id: string;
    tier: Tier;
    deployment: Deployment;
    limits: Limits;
    features: string[];
    issued_at: string;
    expires_at: string;
    superseded_by?: string;
    issued_by: string;
    reason: "new" | "renewal" | "tier_change" | "relink" | "manual";
}

export interface Subscription {
    subscription_id: string;
    account_id: string;
    instance_id?: string;
    tier: Tier;
    term: string;
    status: string;
    current_period_end: string;
}

export interface Plan {
    deployment: Deployment;
    tier: Tier;
    name: string;
    /* The allowance BEFORE anything is bought. Not the total — a metered
     * dimension adds to it. */
    base_limits: Limits;
    base_features: string[];
    support_level: string;
    active: boolean;
}

export interface CatalogueRow {
    kind: "base" | "limit" | "feature";
    deployment: Deployment;
    tier: Tier;
    limit_key?: string;
    feature_key?: string;
    /* environment -> term -> Paddle price ID. The running PADDLE_ENV picks the
     * inner map; both environments are stored so promotion is a config change
     * rather than a data migration. */
    price_ids?: Record<string, Partial<Record<Term, string>>>;
}

export interface EntitlementConfig {
    servers: number;
    features: string[];
}

export interface CheckoutOptions {
    plans: Plan[];
    catalogue: CatalogueRow[];
    env: "sandbox" | "production";
}

/*
 * lineItemsFor builds the Paddle checkout items for a configuration, client-side
 * from the catalogue already fetched. It mirrors the Go catalogue.LineItems and
 * its billable() exactly: base is quantity 1; the per-server unit's quantity is
 * servers MINUS the plan's base allowance (never charge for the base — the one
 * subtraction, kept here to match the server); a feature contributes an item
 * only when its row has a price in this environment/term.
 */
export function lineItemsFor(
    opts: CheckoutOptions,
    choice: { tier: Tier; term: Term; servers: number; features: string[] },
    deployment: Deployment,
): { priceId: string; quantity: number }[] {
    const env = opts.env;
    const plan = opts.plans.find((p) => p.deployment === deployment && p.tier === choice.tier);
    if (!plan) return [];
    const rows = opts.catalogue.filter(
        (r) => r.deployment === deployment && r.tier === choice.tier,
    );
    const priceOf = (r: CatalogueRow) => r.price_ids?.[env]?.[choice.term] ?? "";
    const base = plan.base_limits.max_servers;
    const items: { priceId: string; quantity: number }[] = [];
    for (const r of rows) {
        const id = priceOf(r);
        if (r.kind === "base") {
            if (id) items.push({ priceId: id, quantity: 1 });
        } else if (r.kind === "limit" && r.limit_key === "max_servers") {
            // -1 base is unlimited: nothing metered. Otherwise charge servers over base.
            const qty = base === -1 ? 0 : choice.servers - base;
            if (qty > 0 && id) items.push({ priceId: id, quantity: qty });
        } else if (r.kind === "feature" && r.feature_key) {
            if (choice.features.includes(r.feature_key) && id) {
                items.push({ priceId: id, quantity: 1 });
            }
        }
    }
    return items;
}

export interface Entitlement {
    instance_id: string;
    account_id: string;
    deployment: Deployment;
    tier: Tier;
    term: Term;
    desired: EntitlementConfig;
    granted: EntitlementConfig;
    resolved_limits: Limits;
    scheduled_change_at?: string;
    granted_at: string;
    updated_at: string;
}

export interface CustomerUser {
    user_id: string;
    account_id: string;
    email: string;
    verified_at?: string | null;
    created_at: string;
}

export interface AuditEntry {
    actor: string;
    action: string;
    account_id?: string;
    target?: string;
    detail?: string;
    ip?: string;
    created_at: string;
}

export interface AccountResponse {
    account: Account;
    instances: Instance[];
    max_relinks: number;
}

export interface StaffAccountResponse {
    account: Account;
    instances: Instance[];
    subscriptions: Subscription[];
    users: CustomerUser[];
    audit: AuditEntry[];
}

export type InjectionState = "current" | "stale" | "missing" | "none_issued";

export interface StaffInstanceResponse {
    instance: Instance;
    account: Account;
    licenses: License[];
    injection: { applicable: boolean; state?: InjectionState; failed_at?: string | null };
}

// --- calls ---------------------------------------------------------------

export const api = {
    me: () => req<Session>("/auth/me"),
    login: (email: string, password: string) => post<Session>("/auth/login", { email, password }),
    staffLogin: (email: string, password: string) =>
        post<Session>("/auth/staff/login", { email, password }),
    logout: () => post<{ ok: boolean }>("/auth/logout"),
    verify: (token: string) =>
        req<{ verified: boolean; needs_password?: boolean }>(
            `/auth/verify?token=${encodeURIComponent(token)}`,
        ),

    account: () => req<AccountResponse>("/api/account"),
    link: (instance_id: string, name: string) =>
        post<Instance>("/api/instances/link", { instance_id, name }),
    createInstance: (name: string) => post<Instance>("/api/instances", { name }),
    renewInstance: (id: string) => post<License>(`/api/instances/${id}/renew`, {}),
    // Self-hosted Free: issue the licence on an already-linked instance.
    claimFree: (id: string) => post<License>(`/api/instances/${id}/claim-free`, {}),
    relink: (id: string, instance_id: string) =>
        post<License>(`/api/instances/${id}/relink`, { instance_id }),
    license: (id: string) => req<License & { blob?: string }>(`/api/instances/${id}/license`),
    licenseBlobUrl: (id: string) => `${API_BASE}/api/instances/${id}/license/download`,
    subscriptions: () => req<Subscription[]>("/api/subscriptions"),

    entitlement: (id: string) =>
        req<{ entitlement: Entitlement; pending: boolean }>(`/api/instances/${id}/entitlement`),
    checkoutOptions: () => req<CheckoutOptions>("/api/checkout/options"),
    createSelfHosted: (name: string) =>
        post<{ instance_id: string }>("/api/instances/self-hosted", { name }),
    // Paid cloud: provisions the real instance the paid webhook then licenses.
    createCloudCheckout: (name: string) =>
        post<{ instance_id: string }>("/api/instances/cloud", { name }),
    updateEntitlement: (
        id: string,
        body: { tier: Tier; term: Term; servers: number; features: string[] },
    ) => put<{ entitlement: Entitlement; pending: boolean }>(`/api/instances/${id}/entitlement`, body),
    claimLink: (placeholderId: string, instance_id: string) =>
        post<{ instance_id: string; warning?: string }>(
            `/api/instances/${placeholderId}/claim-link`, { instance_id }),
    billingPortal: () => post<{ url: string }>("/api/billing/portal"),

    accountUsers: () => req<AccountUser[]>("/api/account/users"),
    invite: (email: string, role: AccountRole) =>
        post<{ invited: boolean }>("/api/account/users", { email, role }),
    setAccountRole: (userId: string, role: AccountRole) =>
        put<{ ok: boolean }>(`/api/account/users/${userId}/role`, { role }),
    removeAccountUser: (userId: string) =>
        del<{ deleted: boolean }>(`/api/account/users/${userId}`),
    changePassword: (current_password: string, new_password: string) =>
        put<{ updated: boolean; propagation_pending: boolean }>("/api/account/password", {
            current_password,
            new_password,
        }),
    acceptInvite: (token: string, password: string) =>
        post<{ accepted: boolean }>("/auth/accept-invite", { token, password }),

    members: (instanceId: string) =>
        req<InstanceMember[]>(`/api/instances/${instanceId}/members`),
    grantMember: (instanceId: string, user_id: string, role: InstanceRole) =>
        post<InstanceMember>(`/api/instances/${instanceId}/members`, { user_id, role }),
    setMemberRole: (instanceId: string, userId: string, role: InstanceRole) =>
        put<{ ok: boolean }>(`/api/instances/${instanceId}/members/${userId}/role`, { role }),
    revokeMember: (instanceId: string, userId: string) =>
        del<{ revoked: boolean }>(`/api/instances/${instanceId}/members/${userId}`),

    staff: {
        accounts: (q?: string) =>
            req<Account[]>(`/api/staff/accounts${q ? `?q=${encodeURIComponent(q)}` : ""}`),
        account: (id: string) => req<StaffAccountResponse>(`/api/staff/accounts/${id}`),
        instances: (params?: Record<string, string>) =>
            req<Instance[]>(
                `/api/staff/instances${params ? `?${new URLSearchParams(params)}` : ""}`,
            ),
        instance: (id: string) => req<StaffInstanceResponse>(`/api/staff/instances/${id}`),
        issue: (id: string, payload: { tier: Tier; term?: string; reason?: string }) =>
            post<License>(`/api/staff/instances/${id}/issue`, payload),
        relink: (id: string, instance_id: string) =>
            post<License>(`/api/staff/instances/${id}/relink`, { instance_id }),
        licenses: (params?: Record<string, string>) =>
            req<License[]>(`/api/staff/licenses${params ? `?${new URLSearchParams(params)}` : ""}`),
        plans: () => req<Plan[]>("/api/staff/plans"),
        updatePlan: (deployment: Deployment, tier: Tier, plan: Plan) =>
            put<{ updated: boolean }>(`/api/staff/plans/${deployment}/${tier}`, plan),
        catalogue: () => req<CatalogueRow[]>("/api/staff/catalogue"),
        updateCatalogue: (row: CatalogueRow) =>
            put<{ updated: boolean }>("/api/staff/catalogue", row),
        entitlement: (id: string) =>
            req<{ entitlement: Entitlement; pending: boolean }>(
                `/api/staff/instances/${id}/entitlement`,
            ),
        setEntitlement: (
            id: string,
            body: { tier: Tier; term: Term; servers: number; features: string[]; grant?: boolean },
        ) =>
            put<{ entitlement: Entitlement; pending: boolean }>(
                `/api/staff/instances/${id}/entitlement`, body),
        audit: (accountId?: string) =>
            req<AuditEntry[]>(`/api/staff/audit${accountId ? `?account_id=${accountId}` : ""}`),
        injectionHealth: () =>
            req<{ failed: Instance[]; count: number }>("/api/staff/health/injection"),
        subscriptions: (status?: string) =>
            req<Subscription[]>(`/api/staff/subscriptions${status ? `?status=${status}` : ""}`),
    },
};
