import {createSign} from 'node:crypto';
import type {AccessToken, SalesforceAccessToken, TokenProvider} from './types.js';

type Fetch = typeof fetch;

function base64Url(value: string | Buffer): string {
    return Buffer.from(value).toString('base64url');
}

function signedJwt(header: object, payload: object, privateKey: string): string {
    const unsigned = `${base64Url(JSON.stringify(header))}.${base64Url(JSON.stringify(payload))}`;
    const signer = createSign('RSA-SHA256');
    signer.update(unsigned);
    signer.end();
    return `${unsigned}.${signer.sign(privateKey).toString('base64url')}`;
}

async function postForm(
    fetchImpl: Fetch,
    url: string,
    values: Record<string, string>,
): Promise<Record<string, unknown>> {
    const response = await fetchImpl(url, {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: new URLSearchParams(values),
    });
    const body = await response.text();
    if (!response.ok) {
        throw new Error(`OAuth token request failed (${response.status}): ${body.slice(0, 1000)}`);
    }
    return JSON.parse(body) as Record<string, unknown>;
}

export class SalesforceJwtTokenProvider implements TokenProvider<SalesforceAccessToken> {
    private cached?: SalesforceAccessToken;

    constructor(
        private readonly loginUrl: string,
        private readonly clientId: string,
        private readonly username: string,
        private readonly privateKey: string,
        private readonly fetchImpl: Fetch = fetch,
    ) {}

    async getToken(forceRefresh = false): Promise<SalesforceAccessToken> {
        if (!forceRefresh && this.cached && this.cached.expiresAt > Date.now() + 60_000) {
            return this.cached;
        }
        const now = Math.floor(Date.now() / 1000);
        const assertion = signedJwt(
            {alg: 'RS256', typ: 'JWT'},
            {iss: this.clientId, sub: this.username, aud: this.loginUrl, exp: now + 180},
            this.privateKey,
        );
        const body = await postForm(this.fetchImpl, `${this.loginUrl}/services/oauth2/token`, {
            grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            assertion,
        });
        this.cached = {
            accessToken: String(body.access_token),
            instanceUrl: String(body.instance_url),
            expiresAt: Date.now() + 14 * 60_000,
        };
        return this.cached;
    }
}

export class EntraClientSecretTokenProvider implements TokenProvider {
    private cached?: AccessToken;

    constructor(
        private readonly tenantId: string,
        private readonly clientId: string,
        private readonly clientSecret: string,
        private readonly fetchImpl: Fetch = fetch,
    ) {}

    async getToken(forceRefresh = false): Promise<AccessToken> {
        if (!forceRefresh && this.cached && this.cached.expiresAt > Date.now() + 60_000) {
            return this.cached;
        }
        const tokenUrl = `https://login.microsoftonline.com/${encodeURIComponent(this.tenantId)}/oauth2/v2.0/token`;
        const body = await postForm(this.fetchImpl, tokenUrl, {
            client_id: this.clientId,
            client_secret: this.clientSecret,
            scope: 'https://graph.microsoft.com/.default',
            grant_type: 'client_credentials',
        });
        const expiresIn = Number(body.expires_in ?? 3600);
        this.cached = {
            accessToken: String(body.access_token),
            expiresAt: Date.now() + Math.max(60, expiresIn - 120) * 1000,
        };
        return this.cached;
    }
}
