/**
 * Secure token utilities
 *
 * Crypto via Oslo.js (@oslojs/crypto). Base64url via @oslojs/encoding.
 *
 * Tokens are opaque random values. We store only the SHA-256 hash in the database.
 */

import { sha256 } from "@oslojs/crypto/sha2";
import { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from "@oslojs/encoding";

const TOKEN_BYTES = 32; // 256 bits of entropy

// ---------------------------------------------------------------------------
// API Token Prefixes
// ---------------------------------------------------------------------------

/** Valid API token prefixes */
export const TOKEN_PREFIXES = {
	PAT: "ec_pat_",
	OAUTH_ACCESS: "ec_oat_",
	OAUTH_REFRESH: "ec_ort_",
} as const;

// ---------------------------------------------------------------------------
// Scopes
// ---------------------------------------------------------------------------

/** All valid API token scopes */
export const VALID_SCOPES = [
	"content:read",
	"content:write",
	"media:read",
	"media:write",
	"schema:read",
	"schema:write",
	"admin",
] as const;

export type ApiTokenScope = (typeof VALID_SCOPES)[number];

/**
 * Validate that scopes are all valid.
 * Returns the invalid scopes, or empty array if all valid.
 */
export function validateScopes(scopes: string[]): string[] {
	const validSet = new Set<string>(VALID_SCOPES);
	return scopes.filter((s) => !validSet.has(s));
}

/**
 * Check if a set of scopes includes a required scope.
 * The `admin` scope grants access to everything.
 */
export function hasScope(scopes: string[], required: string): boolean {
	if (scopes.includes("admin")) return true;
	return scopes.includes(required);
}

/**
 * Generate a cryptographically secure random token
 * Returns base64url-encoded string (URL-safe)
 */
export function generateToken(): string {
	const bytes = new Uint8Array(TOKEN_BYTES);
	crypto.getRandomValues(bytes);
	return encodeBase64urlNoPadding(bytes);
}

/**
 * Hash a token for storage
 * We never store raw tokens - only their SHA-256 hash
 */
export function hashToken(token: string): string {
	const bytes = decodeBase64urlIgnorePadding(token);
	const hash = sha256(bytes);
	return encodeBase64urlNoPadding(hash);
}

/**
 * Generate a token and its hash together
 */
export function generateTokenWithHash(): { token: string; hash: string } {
	const token = generateToken();
	const hash = hashToken(token);
	return { token, hash };
}

/**
 * Generate a session ID (shorter, for cookie storage)
 */
export function generateSessionId(): string {
	const bytes = new Uint8Array(20); // 160 bits
	crypto.getRandomValues(bytes);
	return encodeBase64urlNoPadding(bytes);
}

/**
 * Generate an auth secret for configuration
 */
export function generateAuthSecret(): string {
	const bytes = new Uint8Array(32);
	crypto.getRandomValues(bytes);
	return encodeBase64urlNoPadding(bytes);
}

// ---------------------------------------------------------------------------
// Prefixed API tokens (ec_pat_, ec_oat_, ec_ort_)
// ---------------------------------------------------------------------------

/**
 * Generate a prefixed API token and its hash.
 * Returns the raw token (shown once to the user), the hash (stored server-side),
 * and a display prefix (for identification in UIs/logs).
 *
 * Uses oslo/crypto for SHA-256 hashing.
 */
export function generatePrefixedToken(prefix: string): {
	raw: string;
	hash: string;
	prefix: string;
} {
	const bytes = new Uint8Array(TOKEN_BYTES);
	crypto.getRandomValues(bytes);

	const encoded = encodeBase64urlNoPadding(bytes);
	const raw = `${prefix}${encoded}`;
	const hash = hashPrefixedToken(raw);

	// First few chars for identification in UIs
	const displayPrefix = raw.slice(0, prefix.length + 4);

	return { raw, hash, prefix: displayPrefix };
}

/**
 * Hash a prefixed API token for storage/lookup.
 * Hashes the full prefixed token string via SHA-256, returns base64url (no padding).
 */
export function hashPrefixedToken(token: string): string {
	const bytes = new TextEncoder().encode(token);
	const hash = sha256(bytes);
	return encodeBase64urlNoPadding(hash);
}

// ---------------------------------------------------------------------------
// PKCE (RFC 7636) — server-side verification
// ---------------------------------------------------------------------------

/**
 * Compute an S256 PKCE code challenge from a code verifier.
 * Used server-side to verify that code_verifier matches the stored code_challenge.
 *
 * Equivalent to: BASE64URL(SHA256(ASCII(code_verifier)))
 */
export function computeS256Challenge(codeVerifier: string): string {
	const hash = sha256(new TextEncoder().encode(codeVerifier));
	return encodeBase64urlNoPadding(hash);
}

/**
 * Constant-time comparison to prevent timing attacks
 */
export function secureCompare(a: string, b: string): boolean {
	if (a.length !== b.length) return false;

	const aBytes = new TextEncoder().encode(a);
	const bBytes = new TextEncoder().encode(b);

	let result = 0;
	for (let i = 0; i < aBytes.length; i++) {
		result |= aBytes[i]! ^ bBytes[i]!;
	}
	return result === 0;
}

// ============================================================================
// Encryption utilities (for storing OAuth secrets)
// ============================================================================

const ALGORITHM = "AES-GCM";
const IV_BYTES = 12;

/**
 * Derive an encryption key from the auth secret
 */
async function deriveKey(secret: string): Promise<CryptoKey> {
	const decoded = decodeBase64urlIgnorePadding(secret);
	// Create a new ArrayBuffer to ensure compatibility with crypto.subtle
	const buffer = new Uint8Array(decoded).buffer;
	const keyMaterial = await crypto.subtle.importKey("raw", buffer, "PBKDF2", false, ["deriveKey"]);

	return crypto.subtle.deriveKey(
		{
			name: "PBKDF2",
			salt: new TextEncoder().encode("emdash-auth-v1"),
			iterations: 100000,
			hash: "SHA-256",
		},
		keyMaterial,
		{ name: ALGORITHM, length: 256 },
		false,
		["encrypt", "decrypt"],
	);
}

/**
 * Encrypt a value using AES-GCM
 */
export async function encrypt(plaintext: string, secret: string): Promise<string> {
	const key = await deriveKey(secret);
	const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
	const encoded = new TextEncoder().encode(plaintext);

	const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, encoded);

	// Prepend IV to ciphertext
	const combined = new Uint8Array(iv.length + ciphertext.byteLength);
	combined.set(iv);
	combined.set(new Uint8Array(ciphertext), iv.length);

	return encodeBase64urlNoPadding(combined);
}

/**
 * Decrypt a value encrypted with encrypt()
 */
export async function decrypt(encrypted: string, secret: string): Promise<string> {
	const key = await deriveKey(secret);
	const combined = decodeBase64urlIgnorePadding(encrypted);

	const iv = combined.slice(0, IV_BYTES);
	const ciphertext = combined.slice(IV_BYTES);

	const decrypted = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext);

	return new TextDecoder().decode(decrypted);
}
