import crypto from "node:crypto";
import type {
  AuthResponse,
  AuthUser,
  ForgotPasswordResponse,
  ResetPasswordResponse,
  VerifyEmailResponse
} from "@skola/shared";
import { getConfig } from "../config.js";
import { execute, queryRows, withConnection } from "../db/mariadb.js";
import {
  assertAuthEmailConfigured,
  sendPasswordResetEmail,
  sendVerificationEmail
} from "./email-service.js";

type AuthTokenType = "email_verification" | "password_reset" | "session";

interface UserRow {
  id: number;
  email: string;
  name: string;
  password_hash: string;
  email_verified_at: Date | string | null;
}

interface TokenUserRow extends UserRow {
  token_id: number;
  expires_at: Date | string;
  consumed_at: Date | string | null;
}

const SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 14;
const VERIFICATION_TTL_MS = 1000 * 60 * 60 * 24;
const RESET_TTL_MS = 1000 * 60 * 60;
const PASSWORD_ITERATIONS = 180000;
const PASSWORD_KEY_LENGTH = 32;

function normalizeEmail(email: string): string {
  return email.trim().toLowerCase();
}

function assertValidEmail(email: string): void {
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    throw new Error("A valid email address is required.");
  }
}

function assertValidPassword(password: string): void {
  if (password.length < 8) {
    throw new Error("Password must be at least 8 characters.");
  }
}

function hashPassword(password: string): string {
  const salt = crypto.randomBytes(16).toString("base64url");
  const hash = crypto
    .pbkdf2Sync(password, salt, PASSWORD_ITERATIONS, PASSWORD_KEY_LENGTH, "sha256")
    .toString("base64url");
  return `pbkdf2_sha256$${PASSWORD_ITERATIONS}$${salt}$${hash}`;
}

function verifyPassword(password: string, encodedHash: string): boolean {
  const [algorithm, iterationsValue, salt, storedHash] = encodedHash.split("$");
  if (algorithm !== "pbkdf2_sha256" || !iterationsValue || !salt || !storedHash) {
    return false;
  }

  const iterations = Number(iterationsValue);
  if (!Number.isFinite(iterations)) return false;

  const attemptedHash = crypto
    .pbkdf2Sync(password, salt, iterations, PASSWORD_KEY_LENGTH, "sha256")
    .toString("base64url");
  const stored = Buffer.from(storedHash);
  const attempted = Buffer.from(attemptedHash);

  return stored.length === attempted.length && crypto.timingSafeEqual(stored, attempted);
}

function tokenHash(token: string): string {
  return crypto.createHash("sha256").update(token).digest("hex");
}

function createRawToken(): string {
  return crypto.randomBytes(32).toString("base64url");
}

function mysqlDateFromMs(timestamp: number): Date {
  return new Date(timestamp);
}

function isExpired(value: Date | string): boolean {
  return new Date(value).getTime() <= Date.now();
}

function toPublicUser(user: UserRow): AuthUser {
  return {
    email: user.email,
    name: user.name,
    emailVerified: Boolean(user.email_verified_at)
  };
}

function authUrl(path: string, token: string): string {
  const baseUrl = getConfig().authEmail.baseUrl.replace(/\/+$/, "");
  return `${baseUrl}${path}?token=${encodeURIComponent(token)}`;
}

async function findUserByEmail(email: string): Promise<UserRow | null> {
  const rows = await queryRows<UserRow>(
    "SELECT id, email, name, password_hash, email_verified_at FROM users WHERE email = ? LIMIT 1",
    [email]
  );
  return rows[0] ?? null;
}

async function createStoredToken(
  userId: number,
  tokenType: AuthTokenType,
  ttlMs: number
): Promise<string> {
  const token = createRawToken();
  await execute(
    `INSERT INTO auth_tokens (user_id, token_hash, token_type, expires_at)
     VALUES (?, ?, ?, ?)`,
    [userId, tokenHash(token), tokenType, mysqlDateFromMs(Date.now() + ttlMs)]
  );
  return token;
}

async function consumeToken(token: string, tokenType: AuthTokenType): Promise<TokenUserRow> {
  const rows = await queryRows<TokenUserRow>(
    `SELECT
       auth_tokens.id AS token_id,
       auth_tokens.expires_at,
       auth_tokens.consumed_at,
       users.id,
       users.email,
       users.name,
       users.password_hash,
       users.email_verified_at
     FROM auth_tokens
     JOIN users ON users.id = auth_tokens.user_id
     WHERE auth_tokens.token_hash = ?
       AND auth_tokens.token_type = ?
     LIMIT 1`,
    [tokenHash(token), tokenType]
  );
  const row = rows[0];

  if (!row || row.consumed_at || isExpired(row.expires_at)) {
    throw new Error("This secure link is invalid or has expired.");
  }

  await execute("UPDATE auth_tokens SET consumed_at = CURRENT_TIMESTAMP(3) WHERE id = ?", [
    row.token_id
  ]);

  return row;
}

async function sendVerification(user: UserRow): Promise<void> {
  assertAuthEmailConfigured();
  await execute(
    `UPDATE auth_tokens
     SET consumed_at = CURRENT_TIMESTAMP(3)
     WHERE user_id = ?
       AND token_type = 'email_verification'
       AND consumed_at IS NULL`,
    [user.id]
  );
  const token = await createStoredToken(user.id, "email_verification", VERIFICATION_TTL_MS);
  await sendVerificationEmail({
    to: user.email,
    name: user.name,
    verificationUrl: authUrl("/v1/auth/verify-email", token)
  });
}

export async function registerUser(
  email: string,
  password: string,
  name?: string
): Promise<AuthResponse> {
  const normalizedEmail = normalizeEmail(email);
  const normalizedName = name?.trim() || normalizedEmail.split("@")[0] || "Skola user";

  assertValidEmail(normalizedEmail);
  assertValidPassword(password);
  assertAuthEmailConfigured();

  const existingUser = await findUserByEmail(normalizedEmail);
  if (existingUser) {
    throw new Error("An account already exists for this email.");
  }

  const result = await execute(
    `INSERT INTO users (email, name, password_hash)
     VALUES (?, ?, ?)`,
    [normalizedEmail, normalizedName, hashPassword(password)]
  );
  const user: UserRow = {
    id: result.insertId,
    email: normalizedEmail,
    name: normalizedName,
    password_hash: "",
    email_verified_at: null
  };

  await sendVerification(user);

  return {
    user: toPublicUser(user),
    requiresVerification: true,
    message: "Account created. Check your email to verify your Skola account before logging in."
  };
}

export async function loginUser(email: string, password: string): Promise<AuthResponse> {
  const normalizedEmail = normalizeEmail(email);
  assertValidEmail(normalizedEmail);

  const user = await findUserByEmail(normalizedEmail);
  if (!user || !verifyPassword(password, user.password_hash)) {
    throw new Error("Invalid email or password.");
  }

  if (!user.email_verified_at) {
    throw new Error("Email verification required. Check your inbox or request a new verification email.");
  }

  const token = await createStoredToken(user.id, "session", SESSION_TTL_MS);
  await execute("UPDATE users SET last_login_at = CURRENT_TIMESTAMP(3) WHERE id = ?", [user.id]);

  return {
    token,
    user: toPublicUser(user),
    message: "Signed in successfully."
  };
}

export async function verifyEmailToken(token: string): Promise<VerifyEmailResponse> {
  const user = await consumeToken(token.trim(), "email_verification");
  await execute(
    "UPDATE users SET email_verified_at = COALESCE(email_verified_at, CURRENT_TIMESTAMP(3)) WHERE id = ?",
    [user.id]
  );

  return {
    ok: true,
    user: toPublicUser({ ...user, email_verified_at: user.email_verified_at ?? new Date() }),
    message: "Your Skola email address is verified. You can now log in from Word."
  };
}

export async function resendVerificationEmail(email: string): Promise<ForgotPasswordResponse> {
  const normalizedEmail = normalizeEmail(email);
  assertValidEmail(normalizedEmail);

  const user = await findUserByEmail(normalizedEmail);
  if (user && !user.email_verified_at) {
    await sendVerification(user);
  }

  return {
    ok: true,
    message: "If that account exists and still needs verification, Skola has sent a new verification email."
  };
}

export async function requestPasswordReset(email: string): Promise<ForgotPasswordResponse> {
  const normalizedEmail = normalizeEmail(email);
  assertValidEmail(normalizedEmail);

  const user = await findUserByEmail(normalizedEmail);
  if (user) {
    assertAuthEmailConfigured();
    await execute(
      `UPDATE auth_tokens
       SET consumed_at = CURRENT_TIMESTAMP(3)
       WHERE user_id = ?
         AND token_type = 'password_reset'
         AND consumed_at IS NULL`,
      [user.id]
    );
    const token = await createStoredToken(user.id, "password_reset", RESET_TTL_MS);
    await sendPasswordResetEmail({
      to: user.email,
      name: user.name,
      resetUrl: authUrl("/v1/auth/reset-password", token)
    });
  }

  return {
    ok: true,
    message: "If an account exists for that email, Skola has sent password reset instructions."
  };
}

export async function resetPassword(token: string, password: string): Promise<ResetPasswordResponse> {
  assertValidPassword(password);
  const user = await consumeToken(token.trim(), "password_reset");

  await withConnection(async (connection) => {
    await connection.beginTransaction();
    try {
      await connection.query(
        `UPDATE users
         SET password_hash = ?,
             email_verified_at = COALESCE(email_verified_at, CURRENT_TIMESTAMP(3))
         WHERE id = ?`,
        [hashPassword(password), user.id]
      );
      await connection.query(
        `UPDATE auth_tokens
         SET consumed_at = CURRENT_TIMESTAMP(3)
         WHERE user_id = ?
           AND token_type = 'session'
           AND consumed_at IS NULL`,
        [user.id]
      );
      await connection.commit();
    } catch (error) {
      await connection.rollback();
      throw error;
    }
  });

  return {
    ok: true,
    message: "Password reset complete. You can now log in from Word."
  };
}

export async function requireAuthToken(authorization?: string): Promise<AuthUser> {
  const token = authorization?.replace(/^Bearer\s+/i, "").trim() ?? "";
  if (!token) {
    throw new Error("Login required.");
  }

  const rows = await queryRows<TokenUserRow>(
    `SELECT
       auth_tokens.id AS token_id,
       auth_tokens.expires_at,
       auth_tokens.consumed_at,
       users.id,
       users.email,
       users.name,
       users.password_hash,
       users.email_verified_at
     FROM auth_tokens
     JOIN users ON users.id = auth_tokens.user_id
     WHERE auth_tokens.token_hash = ?
       AND auth_tokens.token_type = 'session'
     LIMIT 1`,
    [tokenHash(token)]
  );
  const row = rows[0];

  if (!row || row.consumed_at || isExpired(row.expires_at) || !row.email_verified_at) {
    throw new Error("Login required.");
  }

  return toPublicUser(row);
}
