import { argon2, createHash, randomBytes, timingSafeEqual } from 'node:crypto';

export interface PasswordHasher {
    createSalt(): string;
    hash(password: string): Promise<{ hash: string; salt: string }>;
    verify(password: string, hash: string, salt: string): Promise<{ ok: boolean; needsUpgrade: boolean }>;
}

const ARGON2_MEMORY_KIB = 19 * 1024;
const ARGON2_PASSES = 2;
const ARGON2_PARALLELISM = 1;
const ARGON2_TAG_LENGTH = 32;
const ARGON2_PREFIX = `$argon2id$v=19$m=${ARGON2_MEMORY_KIB},t=${ARGON2_PASSES},p=${ARGON2_PARALLELISM}$`;

const deriveArgon2id = (password: string, salt: Buffer): Promise<Buffer> =>
    new Promise((resolve, reject) => {
        argon2(
            'argon2id',
            {
                message: Buffer.from(password, 'utf8'),
                nonce: salt,
                parallelism: ARGON2_PARALLELISM,
                tagLength: ARGON2_TAG_LENGTH,
                memory: ARGON2_MEMORY_KIB,
                passes: ARGON2_PASSES,
            },
            (error, result) => {
                if (error) {
                    reject(error);
                    return;
                }
                resolve(result);
            }
        );
    });

const safeEqualHex = (left: string, right: string): boolean => {
    if (!/^[a-f0-9]+$/i.test(left) || !/^[a-f0-9]+$/i.test(right) || left.length !== right.length) {
        return false;
    }
    return timingSafeEqual(Buffer.from(left, 'hex'), Buffer.from(right, 'hex'));
};

const parseArgon2Hash = (hash: string): { salt: Buffer; expected: Buffer } | null => {
    if (!hash.startsWith(ARGON2_PREFIX)) {
        return null;
    }
    const encoded = hash.slice(ARGON2_PREFIX.length).split('$');
    if (encoded.length !== 2 || !encoded[0] || !encoded[1]) {
        return null;
    }
    try {
        const salt = Buffer.from(encoded[0], 'base64url');
        const expected = Buffer.from(encoded[1], 'base64url');
        if (salt.length < 16 || expected.length !== ARGON2_TAG_LENGTH) {
            return null;
        }
        return { salt, expected };
    } catch {
        return null;
    }
};

export const createPasswordHasher = (options: { legacyGlobalSalt?: string } = {}): PasswordHasher => ({
    createSalt: () => randomBytes(16).toString('hex'),
    hash: async (password) => {
        const salt = randomBytes(16);
        const derived = await deriveArgon2id(password, salt);
        return {
            hash: `${ARGON2_PREFIX}${salt.toString('base64url')}$${derived.toString('base64url')}`,
            salt: '',
        };
    },
    verify: async (password, hash, salt) => {
        const modern = parseArgon2Hash(hash);
        if (modern) {
            const actual = await deriveArgon2id(password, modern.salt);
            return {
                ok: timingSafeEqual(actual, modern.expected),
                needsUpgrade: false,
            };
        }

        if (/^[a-f0-9]{64}$/i.test(hash)) {
            const actual = createHash('sha256').update(`${salt}:${password}`).digest('hex');
            return { ok: safeEqualHex(actual, hash), needsUpgrade: true };
        }

        if (/^[a-f0-9]{128}$/i.test(hash) && options.legacyGlobalSalt) {
            const browserHash = createHash('sha512')
                .update(`${options.legacyGlobalSalt}${password}${options.legacyGlobalSalt}`)
                .digest('hex');
            const actual = createHash('sha512').update(`${salt}${browserHash}${salt}`).digest('hex');
            return { ok: safeEqualHex(actual, hash), needsUpgrade: true };
        }

        return { ok: false, needsUpgrade: false };
    },
});
