import jwt from 'jsonwebtoken';
import { randomBytes } from 'node:crypto';
import { dev } from '$app/environment';
import { env } from '$env/dynamic/private';
import type { Cookies } from '@sveltejs/kit';

import { db, queries, newId, now, type Account } from './db';

function getJwtSecret(): string {
  const secret = env.JWT_SECRET;
  if (!secret) {
    throw new Error(
      'JWT_SECRET env variable is required. Copy .env.example to .env ' +
        'and generate one with `openssl rand -base64 48`.'
    );
  }
  return secret;
}

const COOKIE_NAME = 'ti_sess';
const COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year

interface SessionPayload {
  sub: string;              // account id
}

export function createSession(cookies: Cookies, accountId: string): void {
  const token = jwt.sign(
    { sub: accountId } satisfies SessionPayload,
    getJwtSecret(),
    { algorithm: 'HS256' }
  );
  cookies.set(COOKIE_NAME, token, {
    path: '/',
    httpOnly: true,
    sameSite: 'lax',
    secure: !dev,
    maxAge: COOKIE_MAX_AGE
  });
}

export function clearSession(cookies: Cookies): void {
  cookies.delete(COOKIE_NAME, { path: '/' });
}

export function getAccountFromCookies(cookies: Cookies): Account | null {
  const raw = cookies.get(COOKIE_NAME);
  if (!raw) return null;
  try {
    const decoded = jwt.verify(raw, getJwtSecret(), {
      algorithms: ['HS256']
    }) as SessionPayload;
    if (!decoded.sub) return null;
    const account = queries.accountById.get(decoded.sub);
    return account ?? null;
  } catch {
    return null;
  }
}

// -------- account helpers --------

/** Create a brand-new anonymous account (no email). */
export function createAnonymousAccount(): Account {
  const id = newId();
  const ts = now();
  queries.insertAccount.run(id, null, 'free', ts);
  return { id, email: null, pending_email: null, plan: 'free', created_at: ts };
}

/** Find an account by email or create a new one with that email. */
export function getOrCreateAccountByEmail(email: string): Account {
  const existing = queries.accountByEmail.get(email);
  if (existing) return existing;
  const id = newId();
  const ts = now();
  queries.insertAccount.run(id, email, 'free', ts);
  return { id, email, pending_email: null, plan: 'free', created_at: ts };
}

// -------- magic links --------

export interface MagicLink {
  token: string;
  expires_at: number;
  url: string;
}

export function generateMagicLink(
  accountId: string,
  baseUrl: string
): MagicLink {
  const token = randomBytes(24).toString('base64url');
  const id = newId();
  const ts = now();
  const expires = ts + 15 * 60 * 1000; // 15 minutes
  queries.insertMagicLink.run(id, accountId, token, expires, ts);
  const url = `${baseUrl.replace(/\/$/, '')}/auth/callback?token=${token}`;
  return { token, expires_at: expires, url };
}

export function consumeMagicLink(token: string): Account | null {
  const row = queries.magicLinkByToken.get(token);
  if (!row) return null;
  if (row.used === 1) return null;
  if (row.expires_at < now()) return null;
  queries.markMagicLinkUsed.run(row.id);
  const account = queries.accountById.get(row.account_id);
  return account ?? null;
}

// -------- email attach / merge --------

/**
 * Attach an email to an existing (anonymous) account. If another account
 * already uses that email, merge this account into the other one — all
 * API keys move over, the anonymous account is deleted, and the caller
 * receives the surviving account id (which may be different from the
 * one passed in).
 */
export function attachEmailToAccount(
  accountId: string,
  email: string
): { accountId: string; merged: boolean } {
  const normalized = email.trim().toLowerCase();
  if (!normalized) throw new Error('Email is empty');

  const current = queries.accountById.get(accountId);
  if (!current) throw new Error('Account not found');

  const existing = queries.accountByEmail.get(normalized);

  // Same account already has this email — no-op.
  if (existing && existing.id === current.id) {
    return { accountId: current.id, merged: false };
  }

  // No other account has this email — just attach it.
  if (!existing) {
    queries.updateAccountEmail.run(normalized, current.id);
    return { accountId: current.id, merged: false };
  }

  // Another account already owns the email. Merge current into existing:
  // move all keys over, then delete the current account.
  const merge = db.transaction(() => {
    queries.reassignKeys.run(existing.id, current.id);
    queries.deleteAccount.run(current.id);
  });
  merge();

  return { accountId: existing.id, merged: true };
}
