import { decodeBase32 } from '@/util/base32';
import { createContext, useCallback, useContext, useEffect, useReducer, useRef, useState } from 'react';

type TOTPAlgorithm = "SHA1" | "SHA256" | "SHA512";

const WEBCRYPTO_ALGOS: Record<TOTPAlgorithm, string> = {
    "SHA1": "SHA-1",
    "SHA256": "SHA-256",
    "SHA512": "SHA-512"
}

export interface TOTPAccount {
    //unique identifier of account
    id: number,

    //user provided account label
    label: string,

    //base32 encoded secret
    secret: string,

    //algorithm used for generating codes
    algorithm: TOTPAlgorithm,

    //length of otp code
    digits: number,

    //validity period for each otp code
    period: number
};

//https://datatracker.ietf.org/doc/html/rfc6238
export async function generateOTP(account: TOTPAccount) {
    const counter = Math.floor(Date.now() / 1000 / account.period);

    //convert counter to bytes
    const dataView = new DataView(new ArrayBuffer(8));
    dataView.setBigUint64(0, BigInt(counter), true);

    const algorithmParams = {
        name: "HMAC",
        hash: WEBCRYPTO_ALGOS[account.algorithm]
    }

    //TODO: cache this
    const key = await crypto.subtle.importKey(
        "raw",
        decodeBase32(account.secret),
        algorithmParams,
        false,
        ["sign"]
    );

    const hmac = new Uint8Array(await crypto.subtle.sign(algorithmParams, key, dataView));
    const offset = hmac[hmac.byteLength - 1] & 0xF;

    const code = (
        ((hmac[offset] & 0x7F) << 24) ||
        ((hmac[offset + 1] & 0xFF) << 16) ||
        ((hmac[offset + 2] & 0xFF) << 8) ||
        (hmac[offset + 3] & 0xFF)
    ) % (10 ** account.digits);

    return code.toString().padStart(account.digits, "0");
}

type AccountUpdaterData = [
    TOTPAccount,
    React.MutableRefObject<string | null>,
    React.Dispatch<React.SetStateAction<[otp: string | null, timeLeft: number]>>
];

let accUpdateInterval: NodeJS.Timeout | null = null;
let accountUpdaters: AccountUpdaterData[] = [];

const updateAccounts =async () => {
    const currentTime = Date.now() / 1000;

    for(const [account, otpRef, setState] of accountUpdaters){
        const timeRemaining = Math.max(0, account.period - Math.floor(currentTime % account.period));
        
        if(otpRef.current === null || timeRemaining <= 1){
            otpRef.current = await generateOTP(account);
        }

        setState([otpRef.current, timeRemaining]);
    }
};

export function useAccount(account: TOTPAccount) {
    if(accUpdateInterval === null){
        accUpdateInterval = setInterval(updateAccounts, 1000);
    }

    const [state, setState] = useState<[otp: string | null, timeLeft: number]>([null, 0]);
    const otpRef = useRef<string | null>(null);

    useEffect(() => {
        const updaterData: AccountUpdaterData = [account, otpRef, setState];
        accountUpdaters.push(updaterData);
        updateAccounts();

        return () => {
            accountUpdaters.splice(accountUpdaters.indexOf(updaterData), 1);
        }
    }, []);

    return state;
}

//React helpers for account management

type AccountReducerAction = { type: "create", account: TOTPAccount }
    | { type: "delete", id: TOTPAccount["id"] };

function accountsReducer(accounts: TOTPAccount[], action: AccountReducerAction): TOTPAccount[] {
    switch(action.type){
        case "create": {
            return [...accounts, action.account];
        }
        case "delete": {
            return accounts.filter(account => account.id !== action.id);
        }
        default: {
            throw new Error(`Unknown accounts reducer action: ${(action as any).type}`);
        }
    }
}

const TOTPAccountsContext = createContext<[
    TOTPAccount[], 
    React.Dispatch<AccountReducerAction>
] | null>(null);

export function useAccounts() {
    const context = useContext(TOTPAccountsContext);
    if(context === null)
        throw new Error("AccountsProvider context not present in this React component's parents");

    return context;
}

export function AccountsProvider({ children }: React.PropsWithChildren) {
    const [accounts, dispatch] = useReducer<typeof accountsReducer>(
        accountsReducer,
        [
            //TODO: testing
            /*{
                id: 1,
                label: "Test",
                secret: "AAAAAAAAAAAAAAAA",
                algorithm: "SHA1",
                digits: 6,
                period: 30
            }*/
        ]
    );

    return (
        <TOTPAccountsContext.Provider value={[accounts, dispatch]}>
            {children}
        </TOTPAccountsContext.Provider>
    );
}

//TODO:
//- save to device (and keychain or other secure enclave)
//- prevent saved codes from optionally appearing in backups
//- server side synchronization across multiple devices
//- encryption or some other increased security for server side synchronization
//  (allow syncing to be disabled or set a backup password or some other E2E)
//- if they're on mobile they should probably be directed to scan qr code on the main page, and then select to alternatively input manually