import { useDbStore } from "@/store/db";
import { deriveDeviceKey, getDeviceId } from "@/util/deviceId";
import type { IDBPDatabase } from "idb";
import { useCallback } from "react";

export interface RawLocalAccount {
  accountId: string;
  label: string;
  email: string;
  profilePicture: string;
  access: { data: number[]; iv: number[] };
  refresh: { data: number[]; iv: number[] };
  updatedAt: string;
}

export interface LocalAccount {
  accountId: string;
  label: string;
  email: string;
  profilePicture: string;
  access: string;
  refresh: string;
  updatedAt: string;
}

export interface CreateAccountRequest {
  accountId: string;
  label: string;
  email: string;
  profilePicture: string;
  access: string;
  refresh: string;
}

export interface UpdateAccountTokensRequest {
  accountId: string;
  access: string;
  refresh: string;
}

export interface UpdateAccountInfoRequest {
  accountId: string;
  label?: string;
  profilePicture?: string;
}

export const getDeviceKey = async () => {
  const deviceId = await getDeviceId();
  const deviceKey = await deriveDeviceKey(deviceId);

  return deviceKey;
};

export const encryptToken = async (token: string) => {
  const encoder = new TextEncoder();

  const iv = crypto.getRandomValues(new Uint8Array(12));

  const deviceKey = await getDeviceKey();

  const cipherText = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    deviceKey,
    encoder.encode(token),
  );

  return {
    data: Array.from(new Uint8Array(cipherText)),
    iv: Array.from(iv),
  };
};

export const decryptToken = async (encrypted: {
  data: number[];
  iv: number[];
}) => {
  const decoder = new TextDecoder();

  const deviceKey = await getDeviceKey();

  const decrypted = await crypto.subtle.decrypt(
    {
      name: "AES-GCM",
      iv: new Uint8Array(encrypted.iv),
    },
    deviceKey,
    new Uint8Array(encrypted.data),
  );

  return decoder.decode(decrypted);
};

export const saveAccount = async (
  db: IDBPDatabase,
  req: CreateAccountRequest,
): Promise<LocalAccount> => {
  const access = await encryptToken(req.access);
  const refresh = await encryptToken(req.refresh);

  const tx = db.transaction("accounts", "readwrite");
  const store = tx.objectStore("accounts");

  await store.put({
    accountId: req.accountId,
    label: req.label,
    email: req.email,
    profilePicture: req.profilePicture,
    access,
    refresh,
    updatedAt: new Date().toISOString(),
  });

  await tx.done;

  const account = await getAccount(db, req.accountId);

  return account as LocalAccount;
};

export const getAllAccounts = async (db: IDBPDatabase) => {
  const tx = db.transaction("accounts", "readonly");
  const store = tx.objectStore("accounts");

  const accounts: RawLocalAccount[] = await store.getAll();

  const results: LocalAccount[] = (
    await Promise.all(
      accounts.map(async (account) => {
        try {
          const accessToken = await decryptToken(account.access);
          const refreshToken = await decryptToken(account.refresh);
          return {
            accountId: account.accountId,
            label: account.label,
            email: account.email,
            profilePicture: account.profilePicture,
            access: accessToken,
            refresh: refreshToken,
            updatedAt: account.updatedAt,
          };
        } catch (err) {
          console.warn(`Failed to decrypt account ${account.label}:`, err);
        }
      }),
    )
  ).filter((acc) => acc !== undefined);

  return results;
};

export const getAccount = async (db: IDBPDatabase, accountId: string) => {
  const tx = db.transaction("accounts", "readonly");
  const store = tx.objectStore("accounts");

  const account: RawLocalAccount = await store.get(accountId);

  await tx.done;

  if (!account) return null;

  const accessToken = await decryptToken(account.access);
  const refreshToken = await decryptToken(account.refresh);
  return {
    accountId: account.accountId,
    label: account.label,
    email: account.email,
    profilePicture: account.profilePicture,
    access: accessToken,
    refresh: refreshToken,
    updatedAt: account.updatedAt,
  };
};

export const getAccountRaw = async (db: IDBPDatabase, accountId: string) => {
  const tx = db.transaction("accounts", "readonly");
  const store = tx.objectStore("accounts");

  const account: RawLocalAccount = await store.get(accountId);

  await tx.done;

  if (!account) return null;

  return account;
};

export const updateAccountTokens = async (
  db: IDBPDatabase,
  req: UpdateAccountTokensRequest,
) => {
  const account = await getAccountRaw(db, req.accountId);

  const access = await encryptToken(req.access);
  const refresh = await encryptToken(req.refresh);

  await db?.put?.("accounts", {
    ...account,
    accountId: req.accountId,
    access,
    refresh,
    updatedAt: new Date().toISOString(),
  });
};

export const updateAccountInfo = async (
  db: IDBPDatabase,
  req: UpdateAccountInfoRequest,
) => {
  const account = await getAccountRaw(db, req.accountId);
  await db?.put?.("accounts", {
    ...account,
    ...req,
  });
};

export const deleteAccount = async (db: IDBPDatabase, accountId: string) => {
  const tx = db.transaction("accounts", "readwrite");
  const store = tx.objectStore("accounts");
  await store.delete(accountId);
  await tx.done;
};

export const useAccountRepo = () => {
  const db = useDbStore((state) => state.db);

  const save = useCallback(
    async (req: CreateAccountRequest) => {
      if (!db) throw new Error("No database connection");

      return saveAccount(db, req);
    },
    [db],
  );

  const loadAll = useCallback(async () => {
    if (!db) throw new Error("No database connection");

    return getAllAccounts(db);
  }, [db]);

  const getOne = useCallback(
    async (accountId: string) => {
      if (!db) throw new Error("No database connection");

      return getAccount(db, accountId);
    },
    [db],
  );

  return { save, loadAll, getOne };
};
