import { DomainError } from './tags.js';
import { RemoteRequestError, RemoteUnauthorizedError, } from './remote-http-client.js';
/**
 * M24 — authenticates c4s against the remote API via OAuth Device Authorization
 * Grant. Login is a human action (no MCP tool): the agent never logs in or
 * touches the device flow. The token is an operational secret kept in
 * `remote_session` and never returned to the UI.
 */
export class RemoteAuthService {
    db;
    client;
    flow = null;
    constructor(db, client) {
        this.db = db;
        this.client = client;
    }
    /** POST /api/remote-account/login/start. */
    async startDeviceFlow() {
        const r = await this.client.startDeviceFlow();
        this.flow = {
            deviceCode: r.device_code,
            interval: r.interval,
            expiresAt: Date.now() + r.expires_in * 1000,
        };
        // device_code is intentionally NOT returned — the backend supplies it when polling.
        return {
            user_code: r.user_code,
            verification_uri: r.verification_uri,
            verification_uri_complete: r.verification_uri_complete,
            interval: r.interval,
            expires_in: r.expires_in,
        };
    }
    /** POST /api/remote-account/login/poll. */
    async pollDeviceFlow() {
        if (!this.flow) {
            throw new DomainError('NO_ACTIVE_FLOW', 'no active device flow; call login/start first');
        }
        const result = await this.client.pollDeviceToken(this.flow.deviceCode);
        if (result.ok) {
            const account = await this.persistSession(result.token);
            this.flow = null;
            return { status: 'authorized', account };
        }
        const status = mapPollStatus(result.error);
        if (status === 'slow_down') {
            this.flow.interval += 5; // RFC 8628: back off by a fixed step
            return { status, interval: this.flow.interval };
        }
        if (status !== 'pending') {
            this.flow = null; // terminal — drop the flow
            return { status, message: result.description };
        }
        return { status }; // pending — keep polling
    }
    /** GET /api/remote-account. Never exposes access_token. */
    getCurrentAccount() {
        const row = this.readSession();
        return row ? toRemoteAccount(row) : { connected: false };
    }
    /** POST /api/remote-account/logout. Idempotent; also aborts an in-flight device flow. */
    logout() {
        this.flow = null;
        this.db.prepare('DELETE FROM remote_session').run();
        return { connected: false };
    }
    /**
     * M26 §4 consumer entry point — fetch the remote project for the
     * `/api/remote-project` proxy. Bearer OPTIONAL since 0.1.32: anonymous and
     * non-owner readers also reach the card (with the public subset). The
     * service is still the single owner of Bearer injection + 401 wipe.
     *
     * Returns:
     *  - `{ kind: 'ok', project }` on 200 (project carries `isOwner`, `description`).
     *  - `{ kind: 'not_found' }` on remote 404 (route surfaces `reason: 'not_found'`).
     *
     * Throws `RemoteUnauthorizedError` after wiping the session on 401 (only
     * possible when a bearer was sent; route maps to 502 SESSION_EXPIRED).
     * Rethrows `RemoteRequestError` on other transport errors / 5xx (route maps
     * to 502 REMOTE_UNAVAILABLE).
     */
    async getRemoteProject(remoteProjectId) {
        const row = this.readSession();
        const token = row?.access_token ?? null;
        try {
            const project = await this.client.getProject(token, remoteProjectId);
            return { kind: 'ok', project };
        }
        catch (err) {
            if (err instanceof RemoteUnauthorizedError) {
                this.db.prepare('DELETE FROM remote_session').run();
                throw err;
            }
            if (err instanceof RemoteRequestError && err.status === 404) {
                return { kind: 'not_found' };
            }
            throw err;
        }
    }
    /**
     * 0.1.32 M25 §1a consumer entry point — PATCH the remote project's name and/or
     * description. Owner-only (peer enforces; we gate the call on a present
     * session so the bearer is available). 401 wipes the session row before
     * rethrowing; the route maps that to 502 SESSION_EXPIRED.
     */
    async updateRemoteProject(remoteProjectId, body) {
        const row = this.readSession();
        if (!row)
            throw new DomainError('NOT_CONNECTED', 'not connected to the remote server');
        try {
            return await this.client.updateProject(row.access_token, remoteProjectId, body);
        }
        catch (err) {
            if (err instanceof RemoteUnauthorizedError) {
                this.db.prepare('DELETE FROM remote_session').run();
            }
            throw err;
        }
    }
    /**
     * M25 consumer entry point — transports a release bundle to the remote with the
     * Bearer injected from the local session. First push (`remoteProjectId === null`)
     * creates the project; subsequent pushes append a release. The access_token never
     * leaves M24. On 401 the single-row `remote_session` is atomically wiped (same as
     * logout) and the `RemoteUnauthorizedError` is re-thrown for M25 to map to 502.
     */
    async pushBundle(input) {
        const row = this.readSession();
        if (!row)
            throw new DomainError('NOT_CONNECTED', 'not connected to the remote server');
        const payload = { tarGzPath: input.tarGzPath, sizeBytes: input.sizeBytes, sha256: input.sha256 };
        try {
            if (input.remoteProjectId === null) {
                const res = await this.client.createProject(row.access_token, payload, input.projectName);
                return {
                    remoteProjectId: res.project.id,
                    remoteReleaseId: res.release.id,
                    remoteReleaseSequence: res.release.sequence,
                    deduplicated: false,
                };
            }
            const res = await this.client.pushRelease(row.access_token, input.remoteProjectId, payload);
            return {
                remoteProjectId: input.remoteProjectId,
                remoteReleaseId: res.release.id,
                remoteReleaseSequence: res.release.sequence,
                deduplicated: res.deduplicated,
            };
        }
        catch (err) {
            if (err instanceof RemoteUnauthorizedError) {
                this.db.prepare('DELETE FROM remote_session').run();
            }
            throw err;
        }
    }
    // --- internals ----------------------------------------------------------
    readSession() {
        return (this.db.prepare('SELECT * FROM remote_session LIMIT 1').get() ?? null);
    }
    /**
     * Fetch the account profile (email/status), then atomically replace the
     * single `remote_session` row. Profile-first so a failed enrich never leaves
     * a half-written row; a 401 here means the freshly-issued token was already
     * rejected — surface as unauthenticated without persisting.
     */
    async persistSession(token) {
        let profile;
        try {
            profile = await this.client.getAccount(token.access_token);
        }
        catch (err) {
            if (err instanceof RemoteUnauthorizedError) {
                throw new DomainError('REMOTE_UNAUTHORIZED', 'remote session expired; log in again');
            }
            throw err;
        }
        const tx = this.db.transaction(() => {
            this.db.prepare('DELETE FROM remote_session').run();
            this.db
                .prepare(`INSERT INTO remote_session
             (access_token, token_id, issued_at, remote_account_id, account_email, account_status)
           VALUES (?, ?, ?, ?, ?, ?)`)
                .run(token.access_token, token.token_id, token.issued_at, token.account_id, profile.email, profile.status);
        });
        tx();
        return this.getCurrentAccount();
    }
}
/** Maps the remote device-token enum onto M24's own poll-status enum. */
function mapPollStatus(error) {
    switch (error) {
        case 'authorization_pending':
            return 'pending';
        case 'slow_down':
            return 'slow_down';
        case 'expired_token':
            return 'expired';
        case 'access_denied':
            return 'denied';
        case 'invalid_grant':
            return 'invalid';
    }
}
function toRemoteAccount(row) {
    return {
        connected: true,
        remoteAccountId: row.remote_account_id,
        accountEmail: row.account_email,
        accountStatus: row.account_status === 'deactivated' ? 'deactivated' : 'active',
        connectedAt: row.connected_at,
    };
}
//# sourceMappingURL=remote-auth.js.map