import { scrypt } from '@noble/hashes/scrypt';
import type { CurrentUser, Env, UserRole } from '../env';
import type { DbUserRow } from '../db/schema';
import { first, mapUser } from '../db/queries';
import { base64ToBytes, bytesToBase64, randomToken, sha256Hex, ulid } from '../utils/id';
import { nowSeconds } from '../utils/time';
import { getSettings } from './settings';

const PASSWORD_N = 32768;
const PASSWORD_R = 8;
const PASSWORD_P = 1;
const PASSWORD_DK_LEN = 32;

export class AuthError extends Error {
  status: number;
  code: string;

  constructor(message: string, status = 400, code = 'AUTH_ERROR') {
    super(message);
    this.status = status;
    this.code = code;
  }
}

function textBytes(value: string): Uint8Array {
  return new TextEncoder().encode(value);
}

function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i += 1) diff |= a[i] ^ b[i];
  return diff === 0;
}

export async function hashPassword(password: string): Promise<string> {
  const salt = new Uint8Array(16);
  crypto.getRandomValues(salt);
  const hash = scrypt(textBytes(password), salt, {
    N: PASSWORD_N,
    r: PASSWORD_R,
    p: PASSWORD_P,
    dkLen: PASSWORD_DK_LEN
  });
  return `scrypt$N=${PASSWORD_N}$r=${PASSWORD_R}$p=${PASSWORD_P}$${bytesToBase64(salt)}$${bytesToBase64(hash)}`;
}

export async function verifyPassword(password: string, stored: string): Promise<boolean> {
  const [scheme, nPart, rPart, pPart, saltB64, hashB64] = stored.split('$');
  if (scheme !== 'scrypt' || !nPart || !rPart || !pPart || !saltB64 || !hashB64) return false;
  const N = Number(nPart.replace('N=', ''));
  const r = Number(rPart.replace('r=', ''));
  const p = Number(pPart.replace('p=', ''));
  if (!Number.isFinite(N) || !Number.isFinite(r) || !Number.isFinite(p)) return false;
  const expected = base64ToBytes(hashB64);
  const actual = scrypt(textBytes(password), base64ToBytes(saltB64), { N, r, p, dkLen: expected.length });
  return constantTimeEqual(actual, expected);
}

export async function countUsers(db: D1Database): Promise<number> {
  const row = await db.prepare('SELECT COUNT(*) AS count FROM wm_users').first<{ count: number }>();
  return row?.count || 0;
}

export async function getUserById(db: D1Database, id: string): Promise<CurrentUser | null> {
  const row = await first<DbUserRow>(db.prepare('SELECT * FROM wm_users WHERE id = ?').bind(id));
  return row ? mapUser(row) : null;
}

export async function getUserRowByIdentifier(db: D1Database, identifier: string): Promise<DbUserRow | null> {
  return first<DbUserRow>(
    db.prepare('SELECT * FROM wm_users WHERE lower(username) = lower(?) OR lower(email) = lower(?)').bind(identifier, identifier)
  );
}

export async function registerUser(
  env: Env['Bindings'],
  input: {
    username: string;
    email: string;
    password: string;
    displayName: string;
    bootstrapToken?: string;
  }
): Promise<CurrentUser> {
  const existingUsers = await countUsers(env.DB);
  const settings = await getSettings(env);
  const isBootstrap = existingUsers === 0 && Boolean(input.bootstrapToken);

  if (existingUsers === 0 && !input.bootstrapToken) {
    throw new AuthError('请先访问 /admin/bootstrap 初始化管理员', 403, 'BOOTSTRAP_REQUIRED');
  }
  if (!isBootstrap && !settings.allow_register) {
    throw new AuthError('注册已关闭', 403, 'REGISTER_CLOSED');
  }
  if (isBootstrap && input.bootstrapToken !== env.ADMIN_BOOTSTRAP_TOKEN) {
    throw new AuthError('Bootstrap Token 不正确', 403, 'BOOTSTRAP_TOKEN_INVALID');
  }

  const role: UserRole = isBootstrap ? 'admin' : settings.default_role;
  const now = nowSeconds();
  const id = ulid();
  const passwordHash = await hashPassword(input.password);
  try {
    await env.DB.prepare(
      `INSERT INTO wm_users(id, username, email, display_name, password_hash, role, status, created_at, updated_at)
       VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?)`
    )
      .bind(id, input.username, input.email.toLowerCase(), input.displayName, passwordHash, role, now, now)
      .run();
  } catch (error) {
    throw new AuthError('用户名或邮箱已被使用', 409, 'USER_EXISTS');
  }

  const user = await getUserById(env.DB, id);
  if (!user) throw new AuthError('创建用户失败', 500, 'USER_CREATE_FAILED');
  return user;
}

export async function loginUser(env: Env['Bindings'], identifier: string, password: string): Promise<CurrentUser> {
  const row = await getUserRowByIdentifier(env.DB, identifier);
  if (!row || !(await verifyPassword(password, row.password_hash))) {
    throw new AuthError('账号或密码不正确', 401, 'BAD_CREDENTIALS');
  }
  if (row.status !== 'active') throw new AuthError('账号不可用', 403, 'USER_BLOCKED');
  await env.DB.prepare('UPDATE wm_users SET last_login_at = ?, updated_at = ? WHERE id = ?')
    .bind(nowSeconds(), nowSeconds(), row.id)
    .run();
  return mapUser(row);
}

export async function createSession(
  env: Env['Bindings'],
  userId: string,
  request: Request
): Promise<{ token: string; tokenHash: string; expiresAt: number; maxAge: number }> {
  const token = randomToken(32);
  const tokenHash = await sha256Hex(token);
  const ttlDays = Number(env.SESSION_TTL_DAYS || '30');
  const maxAge = Math.max(1, ttlDays) * 86400;
  const now = nowSeconds();
  const expiresAt = now + maxAge;
  await env.DB.prepare(
    `INSERT INTO wm_sessions(token_hash, user_id, created_at, expires_at, user_agent, ip)
     VALUES (?, ?, ?, ?, ?, ?)`
  )
    .bind(tokenHash, userId, now, expiresAt, request.headers.get('user-agent') || '', request.headers.get('cf-connecting-ip') || '')
    .run();
  await env.KV.put(`session:${tokenHash}`, userId, { expirationTtl: maxAge });
  return { token, tokenHash, expiresAt, maxAge };
}

export async function getSessionUser(env: Env['Bindings'], token: string): Promise<CurrentUser | null> {
  if (!token) return null;
  const tokenHash = await sha256Hex(token);
  const cachedUserId = await env.KV.get(`session:${tokenHash}`);
  if (cachedUserId) return getUserById(env.DB, cachedUserId);

  const now = nowSeconds();
  const row = await first<{ user_id: string; expires_at: number }>(
    env.DB.prepare('SELECT user_id, expires_at FROM wm_sessions WHERE token_hash = ? AND expires_at > ?').bind(tokenHash, now)
  );
  if (!row) return null;
  await env.KV.put(`session:${tokenHash}`, row.user_id, { expirationTtl: Math.max(60, row.expires_at - now) });
  return getUserById(env.DB, row.user_id);
}

export async function destroySession(env: Env['Bindings'], token: string): Promise<void> {
  if (!token) return;
  const tokenHash = await sha256Hex(token);
  await env.DB.prepare('DELETE FROM wm_sessions WHERE token_hash = ?').bind(tokenHash).run();
  await env.KV.delete(`session:${tokenHash}`);
}

export async function updatePassword(env: Env['Bindings'], user: CurrentUser, oldPassword: string, newPassword: string): Promise<void> {
  const row = await first<DbUserRow>(env.DB.prepare('SELECT * FROM wm_users WHERE id = ?').bind(user.id));
  if (!row || !(await verifyPassword(oldPassword, row.password_hash))) {
    throw new AuthError('旧密码不正确', 401, 'BAD_OLD_PASSWORD');
  }
  await env.DB.prepare('UPDATE wm_users SET password_hash = ?, updated_at = ? WHERE id = ?')
    .bind(await hashPassword(newPassword), nowSeconds(), user.id)
    .run();
}
