import bcrypt from "bcryptjs";
import crypto from "node:crypto";
import jwt from "jsonwebtoken";
import type { Response } from "express";
import { env } from "../../config/env.js";
import { query } from "../../db/pool.js";
import { sendOutboxEmail } from "../../lib/mailer.js";

export interface UserRow {
  id: number;
  email: string;
  name: string;
  role: "admin" | "user";
  status: "active" | "blocked" | "pending_deletion";
  avatar_url: string | null;
  password_hash: string | null;
  google_id: string | null;
  email_verified_at: string | null;
  legal_accepted_at: string | null;
  legal_accepted_version: string | null;
  token_version: number;
  last_login_at: string | null;
  deletion_requested_at: string | null;
  deletion_scheduled_for: string | null;
  deleted_at: string | null;
  created_at: string;
}

export interface TokenPayload {
  sub: number;
  email: string;
  name: string;
  role: "admin" | "user";
  ver: number;
}

export interface PendingGoogleRegistrationPayload {
  purpose: "google_registration";
  googleSub: string;
  email: string;
  name: string;
  avatarUrl: string | null;
}

export function signToken(user: Pick<UserRow, "id" | "email" | "name" | "role" | "token_version">): string {
  return jwt.sign(
    { sub: user.id, email: user.email, name: user.name, role: user.role, ver: user.token_version ?? 1 },
    env.JWT_SECRET,
    { expiresIn: env.JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] }
  );
}

export function verifyToken(token: string): TokenPayload {
  return jwt.verify(token, env.JWT_SECRET) as unknown as TokenPayload;
}

export function signPendingGoogleRegistrationToken(payload: Omit<PendingGoogleRegistrationPayload, "purpose">): string {
  return jwt.sign(
    {
      purpose: "google_registration",
      googleSub: payload.googleSub,
      email: payload.email,
      name: payload.name,
      avatarUrl: payload.avatarUrl,
    },
    env.JWT_SECRET,
    { expiresIn: "15m" }
  );
}

export function verifyPendingGoogleRegistrationToken(token: string): PendingGoogleRegistrationPayload {
  const decoded = jwt.verify(token, env.JWT_SECRET) as PendingGoogleRegistrationPayload;
  if (decoded.purpose !== "google_registration") {
    throw new Error("INVALID_GOOGLE_REGISTRATION_TOKEN");
  }
  return decoded;
}

function isProductionLike(): boolean {
  return env.NODE_ENV === "production";
}

export function setAuthCookie(res: Response, token: string): void {
  res.cookie("garantias_token", token, {
    httpOnly: true,
    secure: isProductionLike(),
    sameSite: "lax",
    path: "/",
    maxAge: 1000 * 60 * 60 * 24 * 7,
  });
}

export function clearAuthCookie(res: Response): void {
  res.cookie("garantias_token", "", {
    httpOnly: true,
    secure: isProductionLike(),
    sameSite: "lax",
    path: "/",
    maxAge: 0,
  });
}

export function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, 10);
}

export function verifyPassword(plain: string, hash: string): Promise<boolean> {
  return bcrypt.compare(plain, hash);
}

export async function countUsers(): Promise<number> {
  const rows = await query<{ count: string }>("SELECT COUNT(*) AS count FROM users WHERE deleted_at IS NULL");
  return parseInt(rows[0]?.count ?? "0", 10);
}

export async function findUserByEmail(email: string): Promise<UserRow | null> {
  const rows = await query<UserRow>("SELECT * FROM users WHERE email = $1 AND deleted_at IS NULL LIMIT 1", [email]);
  return rows[0] ?? null;
}

export async function findUserByGoogleId(googleId: string): Promise<UserRow | null> {
  const rows = await query<UserRow>("SELECT * FROM users WHERE google_id = $1 AND deleted_at IS NULL LIMIT 1", [googleId]);
  return rows[0] ?? null;
}

export async function findUserById(id: number): Promise<UserRow | null> {
  const rows = await query<UserRow>("SELECT * FROM users WHERE id = $1 AND deleted_at IS NULL LIMIT 1", [id]);
  return rows[0] ?? null;
}

export async function bumpTokenVersion(userId: number): Promise<UserRow | null> {
  const rows = await query<UserRow>(
    "UPDATE users SET token_version = token_version + 1 WHERE id = $1 AND deleted_at IS NULL RETURNING *",
    [userId]
  );
  return rows[0] ?? null;
}

export async function createUser(email: string, name: string, password: string): Promise<UserRow> {
  const hash = await hashPassword(password);
  const totalUsers = await countUsers();
  const role = totalUsers === 0 ? "admin" : "user";
  const rows = await query<UserRow>(
    "INSERT INTO users (email, name, password_hash, role) VALUES ($1, $2, $3, $4) RETURNING *",
    [email, name, hash, role]
  );
  return rows[0]!;
}

export function isActiveAccount(user: Pick<UserRow, "status">): boolean {
  return user.status === "active";
}

export function accountAccessMessage(status: UserRow["status"]): string {
  if (status === "blocked") {
    return "La cuenta esta bloqueada.";
  }

  if (status === "pending_deletion") {
    return "La cuenta esta pendiente de eliminacion.";
  }

  return "La cuenta no esta activa.";
}

export async function touchLastLogin(userId: number): Promise<void> {
  await query("UPDATE users SET last_login_at = NOW() WHERE id = $1", [userId]);
}

export async function getLegalAcceptanceState(userId: number): Promise<{ accepted: boolean; currentVersion: string | null }> {
  const rows = await query<{ legal_accepted_at: string | null; legal_accepted_version: string | null }>(
    "SELECT legal_accepted_at, legal_accepted_version FROM users WHERE id = $1 LIMIT 1",
    [userId]
  );
  const row = rows[0] ?? null;
  return {
    accepted: Boolean(row?.legal_accepted_at && row?.legal_accepted_version === "1.0"),
    currentVersion: row?.legal_accepted_version ?? null,
  };
}

export async function markLegalAccepted(userId: number, version = "1.0"): Promise<void> {
  await query(
    `UPDATE users
     SET legal_accepted_at = COALESCE(legal_accepted_at, NOW()),
         legal_accepted_version = $2
     WHERE id = $1 AND deleted_at IS NULL`,
    [userId, version]
  );
}

export async function getAccountState(userId: number): Promise<{ user: UserRow; hasPassword: boolean } | null> {
  const user = await findUserById(userId);
  if (!user) return null;
  return { user, hasPassword: Boolean(user.password_hash) };
}

export async function updateAccountProfile(input: {
  userId: number;
  name: string;
  email?: string;
  currentPassword?: string;
}): Promise<{ user: UserRow; requiresEmailVerification: boolean }> {
  const current = await findUserById(input.userId);
  if (!current) {
    throw new Error("USER_NOT_FOUND");
  }

  const nextName = input.name.trim();
  const nextEmail = input.email?.trim().toLowerCase() ?? current.email;
  const emailChanged = nextEmail !== current.email;

  if (emailChanged) {
    if (!current.password_hash) {
      throw new Error("PASSWORD_REQUIRED");
    }
    if (!input.currentPassword) {
      throw new Error("CURRENT_PASSWORD_REQUIRED");
    }
    const passwordOk = await verifyPassword(input.currentPassword, current.password_hash);
    if (!passwordOk) {
      throw new Error("BAD_PASSWORD");
    }

    const existing = await query<{ id: number }>(
      "SELECT id FROM users WHERE email = $1 AND id <> $2 AND deleted_at IS NULL LIMIT 1",
      [nextEmail, input.userId]
    );
    if (existing[0]) {
      throw new Error("EMAIL_EXISTS");
    }
  }

  const updatedRows = await query<UserRow>(
    `UPDATE users
     SET name = $1,
         email = $2,
         email_verified_at = CASE WHEN $3 THEN NULL ELSE email_verified_at END
     WHERE id = $4 AND deleted_at IS NULL
     RETURNING *`,
    [nextName, nextEmail, emailChanged, input.userId]
  );

  const updated = updatedRows[0];
  if (!updated) {
    throw new Error("USER_NOT_FOUND");
  }

  if (emailChanged) {
    await createEmailVerification(updated);
  }

  return { user: updated, requiresEmailVerification: emailChanged };
}

export async function updateAccountPassword(input: {
  userId: number;
  currentPassword: string;
  newPassword: string;
}): Promise<UserRow> {
  const current = await findUserById(input.userId);
  if (!current) {
    throw new Error("USER_NOT_FOUND");
  }

  if (!current.password_hash) {
    throw new Error("PASSWORD_NOT_AVAILABLE");
  }

  const passwordOk = await verifyPassword(input.currentPassword, current.password_hash);
  if (!passwordOk) {
    throw new Error("BAD_PASSWORD");
  }

  const hash = await hashPassword(input.newPassword);
  const updatedRows = await query<UserRow>(
    "UPDATE users SET password_hash = $1 WHERE id = $2 AND deleted_at IS NULL RETURNING *",
    [hash, input.userId]
  );

  const updated = updatedRows[0];
  if (!updated) {
    throw new Error("USER_NOT_FOUND");
  }

  return updated;
}

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

export async function createEmailVerification(user: Pick<UserRow, "id" | "email" | "name">): Promise<string> {
  const token = crypto.randomBytes(32).toString("base64url");
  const tokenHash = hashVerificationToken(token);
  const verifyUrl = `${env.WEB_BASE_URL}/login?verify=${encodeURIComponent(token)}`;

  await query(
    `INSERT INTO email_verification_tokens (user_id, token_hash, expires_at)
     VALUES ($1, $2, NOW() + INTERVAL '24 hours')`,
    [user.id, tokenHash]
  );

  const outbox = await query<{ id: number }>(
    `INSERT INTO email_outbox (user_id, recipient, subject, body)
     VALUES ($1, $2, $3, $4)
     RETURNING id`,
    [
      user.id,
      user.email,
      "Verifica tu cuenta de TikeTrack",
      `Hola ${user.name}, verifica tu cuenta entrando en este enlace: ${verifyUrl}`,
    ]
  );

  const outboxId = outbox[0]?.id;
  if (outboxId) {
    void sendOutboxEmail({
      id: outboxId,
      recipient: user.email,
      subject: "Verifica tu cuenta de TikeTrack",
      body: `Hola ${user.name}, verifica tu cuenta entrando en este enlace: ${verifyUrl}`,
    });
  }

  return verifyUrl;
}

export async function verifyEmailToken(token: string): Promise<UserRow | null> {
  const tokenHash = hashVerificationToken(token);
  const rows = await query<{ id: number; user_id: number }>(
    `SELECT id, user_id
     FROM email_verification_tokens
     WHERE token_hash = $1
       AND consumed_at IS NULL
       AND expires_at > NOW()
     LIMIT 1`,
    [tokenHash]
  );

  const tokenRow = rows[0];
  if (!tokenRow) return null;

  await query(
    "UPDATE email_verification_tokens SET consumed_at = NOW() WHERE id = $1",
    [tokenRow.id]
  );

  const updated = await query<UserRow>(
    "UPDATE users SET email_verified_at = COALESCE(email_verified_at, NOW()) WHERE id = $1 AND deleted_at IS NULL RETURNING *",
    [tokenRow.user_id]
  );

  return updated[0] ?? null;
}

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

export async function createPasswordResetRequest(email: string): Promise<boolean> {
  const user = await findUserByEmail(email);
  if (!user || !user.password_hash || !isActiveAccount(user)) {
    return false;
  }

  const token = crypto.randomBytes(32).toString("base64url");
  const tokenHash = hashResetToken(token);
  const resetUrl = `${env.WEB_BASE_URL}/reset-password?token=${encodeURIComponent(token)}`;

  await query(
    `INSERT INTO password_reset_tokens (user_id, token_hash, expires_at)
     VALUES ($1, $2, NOW() + INTERVAL '1 hour')`,
    [user.id, tokenHash]
  );

  const outbox = await query<{ id: number }>(
    `INSERT INTO email_outbox (user_id, recipient, subject, body)
     VALUES ($1, $2, $3, $4)
     RETURNING id`,
    [
      user.id,
      user.email,
      "Recupera tu contraseña de TikeTrack",
      `Hola ${user.name}, recupera tu contraseña entrando en este enlace: ${resetUrl}`,
    ]
  );

  const outboxId = outbox[0]?.id;
  if (outboxId) {
    void sendOutboxEmail({
      id: outboxId,
      recipient: user.email,
      subject: "Recupera tu contraseña de TikeTrack",
      body: `Hola ${user.name}, recupera tu contraseña entrando en este enlace: ${resetUrl}`,
    });
  }

  return true;
}

export async function resetPasswordWithToken(token: string, password: string): Promise<UserRow | null> {
  const tokenHash = hashResetToken(token);
  const rows = await query<{ id: number; user_id: number }>(
    `SELECT id, user_id
     FROM password_reset_tokens
     WHERE token_hash = $1
       AND consumed_at IS NULL
       AND expires_at > NOW()
     LIMIT 1`,
    [tokenHash]
  );

  const tokenRow = rows[0];
  if (!tokenRow) return null;

  await query(
    "UPDATE password_reset_tokens SET consumed_at = NOW() WHERE id = $1",
    [tokenRow.id]
  );

  const hash = await hashPassword(password);
  const updated = await query<UserRow>(
    "UPDATE users SET password_hash = $1 WHERE id = $2 AND deleted_at IS NULL RETURNING *",
    [hash, tokenRow.user_id]
  );

  return updated[0] ?? null;
}

export async function findOrCreateGoogleUser(
  googleId: string,
  email: string,
  name: string,
  avatarUrl?: string | null
): Promise<UserRow> {
  const byGid = await query<UserRow>("SELECT * FROM users WHERE google_id = $1 LIMIT 1", [googleId]);
  if (byGid[0]) return byGid[0];

  const byEmail = await query<UserRow>("SELECT * FROM users WHERE email = $1 LIMIT 1", [email]);
  if (byEmail[0]) {
    const updated = await query<UserRow>(
      "UPDATE users SET google_id=$1, avatar_url=COALESCE($2,avatar_url), email_verified_at=COALESCE(email_verified_at,NOW()) WHERE id=$3 AND deleted_at IS NULL RETURNING *",
      [googleId, avatarUrl ?? null, byEmail[0].id]
    );
    return updated[0]!;
  }

  const totalUsers = await countUsers();
  const role = totalUsers === 0 ? "admin" : "user";
  const created = await query<UserRow>(
    "INSERT INTO users (email, name, google_id, avatar_url, role, email_verified_at) VALUES ($1,$2,$3,$4,$5,NOW()) RETURNING *",
    [email, name, googleId, avatarUrl ?? null, role]
  );
  return created[0]!;
}

export function safeUser(u: UserRow) {
  return {
    id: u.id,
    email: u.email,
    name: u.name,
    role: u.role,
    status: u.status,
    emailVerified: Boolean(u.email_verified_at),
    avatarUrl: u.avatar_url,
  };
}
