import type { Request } from "express";
import { one, query, transaction } from "../../db/client.js";
import { config } from "../../config.js";
import { hashPassword, randomToken, sha256, verifyPassword } from "../../lib/crypto.js";
import { audit } from "../../lib/audit.js";
import { badRequest, HttpError, unauthorized } from "../../lib/errors.js";

interface UserRow {
  id: number;
  username: string;
  full_name: string;
  email: string | null;
  password_hash: string;
  is_active: boolean;
  must_change_password: boolean;
  is_demo: boolean;
  failed_login_count: number;
  locked_until: string | null;
}

async function settingInt(key: string, fallback: number): Promise<number> {
  const row = await one<{ value: string }>(`SELECT value FROM settings WHERE key = $1`, [key]);
  const parsed = row ? Number.parseInt(row.value, 10) : NaN;
  return Number.isNaN(parsed) ? fallback : parsed;
}

export interface LoginMeta {
  ip: string | null;
  userAgent: string | null;
}

export interface LoginResult {
  token: string;
  user: {
    id: number;
    username: string;
    fullName: string;
    mustChangePassword: boolean;
    isDemo: boolean;
  };
}

export async function login(
  rawUsername: string,
  password: string,
  meta: LoginMeta,
): Promise<LoginResult> {
  const username = rawUsername.trim().toLowerCase();
  const user = await one<UserRow>(`SELECT * FROM users WHERE lower(username) = $1`, [username]);

  const genericFail = async (reason: string, actorUserId: number | null = null) => {
    await audit({
      actorUserId,
      action: "auth.login.failed",
      entityType: "user",
      context: { reason },
      ip: meta.ip,
      userAgent: meta.userAgent,
    });
    throw unauthorized("بيانات الدخول غير صحيحة");
  };

  if (!user) {
    await genericFail("unknown_username");
    throw unreachable();
  }

  if (user.locked_until && new Date(user.locked_until) > new Date()) {
    await audit({
      actorUserId: user.id,
      action: "auth.login.locked",
      entityType: "user",
      entityId: user.id,
      ip: meta.ip,
      userAgent: meta.userAgent,
    });
    throw new HttpError(423, "الحساب مقفل مؤقتًا بسبب محاولات دخول فاشلة متكررة — حاول لاحقًا", "account_locked");
  }

  const ok = await verifyPassword(password, user.password_hash);
  if (!ok) {
    const maxAttempts = await settingInt("security.lockout_max_attempts", config.lockout.maxAttempts);
    const newCount = user.failed_login_count + 1;
    const lock = newCount >= maxAttempts;
    const lockMinutes = await settingInt("security.lockout_minutes", config.lockout.minutes);
    await query(
      `UPDATE users
         SET failed_login_count = $2,
             locked_until = CASE WHEN $3 THEN now() + make_interval(mins => $4) ELSE locked_until END,
             updated_at = now()
       WHERE id = $1`,
      [user.id, newCount, lock, lockMinutes],
    );
    await genericFail(lock ? "locked_after_failures" : "wrong_password", user.id);
    throw unreachable();
  }

  if (!user.is_active) {
    await audit({
      actorUserId: user.id,
      action: "auth.login.inactive",
      entityType: "user",
      entityId: user.id,
      ip: meta.ip,
      userAgent: meta.userAgent,
    });
    throw unauthorized("الحساب غير مفعّل — راجع مسؤول النظام");
  }

  // Success: reset counters, issue session
  const token = randomToken(32);
  const ttlMinutes = config.session.ttlMinutes;
  await transaction(async (client) => {
    await client.query(
      `UPDATE users SET failed_login_count = 0, locked_until = NULL, last_login_at = now(), updated_at = now() WHERE id = $1`,
      [user.id],
    );
    await client.query(
      `INSERT INTO sessions (id, user_id, expires_at, ip_address, user_agent)
       VALUES ($1, $2, now() + make_interval(mins => $3), $4::inet, $5)`,
      [sha256(token), user.id, ttlMinutes, meta.ip, meta.userAgent?.slice(0, 255) ?? null],
    );
    await audit(
      {
        actorUserId: user.id,
        action: "auth.login.success",
        entityType: "user",
        entityId: user.id,
        ip: meta.ip,
        userAgent: meta.userAgent,
      },
      client,
    );
  });

  return {
    token,
    user: {
      id: user.id,
      username: user.username,
      fullName: user.full_name,
      mustChangePassword: user.must_change_password,
      isDemo: user.is_demo,
    },
  };
}

export async function logout(sessionId: string, userId: number, meta: LoginMeta): Promise<void> {
  await query(`UPDATE sessions SET revoked_at = now() WHERE id = $1`, [sessionId]);
  await audit({
    actorUserId: userId,
    action: "auth.logout",
    entityType: "user",
    entityId: userId,
    ip: meta.ip,
    userAgent: meta.userAgent,
  });
}

export async function changePassword(
  req: Request,
  userId: number,
  currentPassword: string,
  newPassword: string,
): Promise<void> {
  const user = await one<{ password_hash: string }>(`SELECT password_hash FROM users WHERE id = $1`, [userId]);
  if (!user) throw unauthorized();
  if (!(await verifyPassword(currentPassword, user.password_hash))) {
    await audit({
      actorUserId: userId,
      action: "auth.password.change_failed",
      entityType: "user",
      entityId: userId,
      ip: req.ip ?? null,
      userAgent: req.headers["user-agent"] ?? null,
    });
    throw badRequest("كلمة المرور الحالية غير صحيحة", "wrong_password");
  }
  const hash = await hashPassword(newPassword);
  await transaction(async (client) => {
    await client.query(
      `UPDATE users SET password_hash = $2, must_change_password = FALSE, updated_at = now() WHERE id = $1`,
      [userId, hash],
    );
    // Invalidate every session except the current one
    await client.query(`UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND id <> $2 AND revoked_at IS NULL`, [
      userId,
      req.sessionId as string,
    ]);
    await audit(
      {
        actorUserId: userId,
        action: "auth.password.changed",
        entityType: "user",
        entityId: userId,
        ip: req.ip ?? null,
        userAgent: req.headers["user-agent"] ?? null,
      },
      client,
    );
  });
}

function unreachable(): never {
  throw new Error("unreachable");
}
