import { randomBytes } from 'crypto';
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
import { dirname, join } from 'path';
import jwt from 'jsonwebtoken';
import { getCCWHome } from '../../config/storage-paths.js';
const DEFAULT_TOKEN_TTL_MS = 24 * 60 * 60 * 1000;
const DEFAULT_ROTATE_BEFORE_EXPIRY_MS = 60 * 60 * 1000;
const JWT_ALGORITHM = 'HS256';
function ensureDirectory(dirPath) {
    if (!existsSync(dirPath)) {
        mkdirSync(dirPath, { recursive: true });
    }
}
function bestEffortRestrictPermissions(filePath, mode) {
    try {
        chmodSync(filePath, mode);
    }
    catch {
        // Ignore permission errors (e.g., Windows or restrictive environments)
    }
}
function writeSecretFile(filePath, content) {
    ensureDirectory(dirname(filePath));
    writeFileSync(filePath, content, { encoding: 'utf8', mode: 0o600 });
    bestEffortRestrictPermissions(filePath, 0o600);
}
function writeTokenFile(filePath, content) {
    ensureDirectory(dirname(filePath));
    writeFileSync(filePath, content, { encoding: 'utf8', mode: 0o600 });
    bestEffortRestrictPermissions(filePath, 0o600);
}
function parseJwtExpiry(token) {
    const decoded = jwt.decode(token);
    if (!decoded || typeof decoded !== 'object')
        return null;
    if (typeof decoded.exp !== 'number')
        return null;
    return new Date(decoded.exp * 1000);
}
export class TokenManager {
    authDir;
    secretKeyPath;
    tokenPath;
    tokenTtlMs;
    rotateBeforeExpiryMs;
    secretKey = null;
    activeTokens = new Map();
    constructor(options = {}) {
        this.authDir = options.authDir ?? join(getCCWHome(), 'auth');
        this.secretKeyPath = options.secretKeyPath ?? join(this.authDir, 'secret.key');
        this.tokenPath = options.tokenPath ?? join(this.authDir, 'token.jwt');
        this.tokenTtlMs = options.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
        this.rotateBeforeExpiryMs = options.rotateBeforeExpiryMs ?? DEFAULT_ROTATE_BEFORE_EXPIRY_MS;
    }
    getSecretKey() {
        if (this.secretKey)
            return this.secretKey;
        ensureDirectory(this.authDir);
        if (existsSync(this.secretKeyPath)) {
            const loaded = readFileSync(this.secretKeyPath, 'utf8').trim();
            if (!loaded) {
                throw new Error('Auth secret key file is empty');
            }
            this.secretKey = loaded;
            return loaded;
        }
        const generated = randomBytes(32).toString('hex');
        writeSecretFile(this.secretKeyPath, generated);
        this.secretKey = generated;
        return generated;
    }
    generateToken(secretKey) {
        const token = jwt.sign({
            typ: 'ccw-api',
            jti: randomBytes(16).toString('hex'),
        }, secretKey, {
            algorithm: JWT_ALGORITHM,
            expiresIn: Math.floor(this.tokenTtlMs / 1000),
        });
        const expiresAt = parseJwtExpiry(token) ?? new Date(Date.now() + this.tokenTtlMs);
        this.activeTokens.set(token, { token, expiresAt, issuedAt: new Date() });
        return { token, expiresAt };
    }
    validateToken(token, secretKey) {
        const info = this.activeTokens.get(token);
        if (info?.revokedAt)
            return false;
        try {
            jwt.verify(token, secretKey, { algorithms: [JWT_ALGORITHM] });
            return true;
        }
        catch {
            return false;
        }
    }
    refreshToken(token, secretKey) {
        const existing = this.activeTokens.get(token);
        if (existing) {
            existing.revokedAt = new Date();
        }
        const next = this.generateToken(secretKey);
        if (existing) {
            existing.rotatedAt = new Date();
            existing.replacedBy = next.token;
        }
        return next;
    }
    /**
     * Read an existing persisted token or create a new one.
     * If the existing token is nearing expiry, rotate it.
     */
    getOrCreateAuthToken() {
        const secretKey = this.getSecretKey();
        if (existsSync(this.tokenPath)) {
            const persisted = readFileSync(this.tokenPath, 'utf8').trim();
            if (persisted && this.validateToken(persisted, secretKey)) {
                const expiresAt = parseJwtExpiry(persisted);
                if (expiresAt) {
                    // Ensure persisted token is tracked for revocation support
                    if (!this.activeTokens.has(persisted)) {
                        this.activeTokens.set(persisted, { token: persisted, expiresAt, issuedAt: new Date() });
                    }
                    const msUntilExpiry = expiresAt.getTime() - Date.now();
                    if (msUntilExpiry > this.rotateBeforeExpiryMs) {
                        return { token: persisted, expiresAt };
                    }
                }
                // Token exists but is expiring soon (or expiry missing) → rotate
                const rotated = this.generateToken(secretKey);
                writeTokenFile(this.tokenPath, rotated.token);
                const existing = this.activeTokens.get(persisted);
                if (existing) {
                    existing.rotatedAt = new Date();
                    existing.replacedBy = rotated.token;
                }
                return rotated;
            }
        }
        const created = this.generateToken(secretKey);
        writeTokenFile(this.tokenPath, created.token);
        return created;
    }
    revokeToken(token) {
        const info = this.activeTokens.get(token);
        if (info) {
            info.revokedAt = new Date();
        }
        else {
            this.activeTokens.set(token, {
                token,
                issuedAt: new Date(),
                expiresAt: new Date(0),
                revokedAt: new Date(),
            });
        }
    }
}
let tokenManagerInstance = null;
export function getTokenManager(options) {
    if (!tokenManagerInstance) {
        tokenManagerInstance = new TokenManager(options);
    }
    return tokenManagerInstance;
}
export function resetTokenManager() {
    tokenManagerInstance = null;
}
export function getOrCreateAuthToken() {
    return getTokenManager().getOrCreateAuthToken();
}
//# sourceMappingURL=token-manager.js.map