// ─────────────────────────────────────────────────────────────────────────────
// Authentication: password hashing, session tokens & user lookup
//
// Provides the full auth layer for the CRM:
//   • Password hashing & comparison (bcryptjs, 12 rounds)
//   • User queries (by email, username, ID)
//   • Brute-force protection (failed-attempt tracking, lockout)
//   • Session creation / restoration via JWT cookies
//   • Encrypt / decrypt helpers for stored secrets (delegated to PG functions)
// ─────────────────────────────────────────────────────────────────────────────

import bcrypt from "bcryptjs";
import { query } from "@/lib/db";
import { cookies } from "next/headers";
import { signToken, verifyToken } from "@/lib/jwt";

// ── Constants ────────────────────────────────────────────────────────────────

export const SESSION_COOKIE = "session";
const MAX_FAILED_ATTEMPTS = 5;
const LOCKOUT_DURATION_MINUTES = 15;

// ── Types ────────────────────────────────────────────────────────────────────

export interface SessionUser {
  id: string;
  username: string;
  email: string;
  phone: string | null;
  firstName: string;
  lastName: string;
  role: string;
  avatar: string;
}

// ── Password helpers ─────────────────────────────────────────────────────────

/**
 * Hash a plaintext password with bcrypt (12 salt rounds).
 *
 * @param password - Raw password string.
 * @returns        The bcrypt hash.
 */
export async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, 12);
}

/**
 * Compare a plaintext password against a bcrypt hash.
 *
 * @param password - Raw password string.
 * @param hash     - Previously generated bcrypt hash.
 * @returns        `true` when the password matches.
 */
export async function comparePassword(
  password: string,
  hash: string,
): Promise<boolean> {
  return bcrypt.compare(password, hash);
}

// ── User queries ─────────────────────────────────────────────────────────────

/**
 * Look up a (non-deleted) user by their **email**.
 *
 * Joins `users` → `user_roles` → `roles` to include the role name.  The email
 * is lower-cased and trimmed before querying.
 *
 * @param email - The user's email address.
 * @returns     A full user row (including `password_hash`, lock state, …) or
 *              `null` when not found.
 */
export async function getUserByEmail(email: string) {
  const result = await query(
    `    SELECT u.id, u.username, u.email, u.password_hash, u.first_name, u.last_name,
            u.is_active, u.is_locked, u.failed_login_attempts, u.locked_until,
            u.password_change_required, u.avatar_url,
            r.name AS role_name
     FROM users u
     JOIN user_roles ur ON ur.user_id = u.id
     JOIN roles r ON r.id = ur.role_id
     WHERE u.email = $1 AND u.deleted_at IS NULL
     LIMIT 1`,
    [email.toLowerCase().trim()],
  );
  return result.rows[0] || null;
}

/**
 * Look up a (non-deleted) user by their **username**.
 *
 * Same join pattern as `getUserByEmail`. The username is lower-cased and
 * trimmed before querying.
 *
 * @param username - The user's username.
 * @returns        A full user row or `null`.
 */
export async function getUserByUsername(username: string) {
  const result = await query(
    `    SELECT u.id, u.username, u.email, u.password_hash, u.first_name, u.last_name,
            u.is_active, u.is_locked, u.failed_login_attempts, u.locked_until,
            u.password_change_required, u.avatar_url,
            r.name AS role_name
     FROM users u
     JOIN user_roles ur ON ur.user_id = u.id
     JOIN roles r ON r.id = ur.role_id
     WHERE u.username = $1 AND u.deleted_at IS NULL
     LIMIT 1`,
    [username.toLowerCase().trim()],
  );
  return result.rows[0] || null;
}

/**
 * Look up a (non-deleted) user by their **UUID**.
 *
 * Returns a subset of fields safe for session construction (no password hash,
 * no lock state).
 *
 * @param id - The user's UUID.
 * @returns  A safe user row or `null`.
 */
export async function getUserById(id: string) {
  const result = await query(
    `    SELECT u.id, u.username, u.email, u.phone, u.first_name, u.last_name,
            u.is_active, u.avatar_url,
            r.name AS role_name
     FROM users u
     JOIN user_roles ur ON ur.user_id = u.id
     JOIN roles r ON r.id = ur.role_id
     WHERE u.id = $1 AND u.deleted_at IS NULL
     LIMIT 1`,
    [id],
  );
  return result.rows[0] || null;
}

// ── Login-attempt tracking ───────────────────────────────────────────────────

/**
 * Insert a row into the `login_attempts` audit log.
 *
 * @param userId           - The user's UUID (may be `null` on failed lookups).
 * @param usernameAttempted - Raw username/email submitted.
 * @param ipAddress        - Client IP.
 * @param userAgent        - Client User-Agent string.
 * @param wasSuccessful    - Whether the login succeeded.
 * @param failureReason    - Optional human-readable failure reason.
 */
export async function recordLoginAttempt(
  userId: string | null,
  usernameAttempted: string,
  ipAddress: string,
  userAgent: string | null,
  wasSuccessful: boolean,
  failureReason?: string,
) {
  await query(
    `INSERT INTO login_attempts (user_id, username_attempted, ip_address, user_agent, was_successful, failure_reason)
     VALUES ($1, $2, $3, $4, $5, $6)`,
    [
      userId,
      usernameAttempted,
      ipAddress,
      userAgent,
      wasSuccessful,
      failureReason || null,
    ],
  );
}

/**
 * Increment the user's `failed_login_attempts` counter and conditionally set a
 * `locked_until` timestamp when the threshold (`MAX_FAILED_ATTEMPTS`) is met
 * or exceeded.
 *
 * The `locked_until` column is set to `NOW() + LOCKOUT_DURATION_MINUTES` only
 * when the **new** count reaches or passes the threshold.
 *
 * @param userId - The user's UUID.
 */
export async function incrementFailedAttempts(userId: string) {
  await query(
    `UPDATE users
     SET failed_login_attempts = failed_login_attempts + 1,
         locked_until = CASE
           WHEN failed_login_attempts + 1 >= $2
            THEN NOW() + (INTERVAL '1 minute' * $3)
           ELSE locked_until
         END
     WHERE id = $1`,
    [userId, MAX_FAILED_ATTEMPTS, LOCKOUT_DURATION_MINUTES],
  );
}

/**
 * Reset `failed_login_attempts` to 0, clear `locked_until`, and set
 * `last_login_at` to the current time.
 *
 * @param userId - The user's UUID.
 */
export async function resetFailedAttempts(userId: string) {
  await query(
    `UPDATE users
     SET failed_login_attempts = 0,
         locked_until = NULL,
         last_login_at = NOW()
     WHERE id = $1`,
    [userId],
  );
}

/**
 * Check whether a user's account is currently locked.
 *
 * Two conditions are checked:
 *   1. `is_locked` flag (admin-initiated lock).
 *   2. `locked_until` still in the future (temporary brute-force lockout).
 *
 * @param user - Object containing `is_locked` and `locked_until` fields.
 * @returns    `{ locked: false }` or `{ locked: true, reason: string }`.
 */
export async function isAccountLocked(user: {
  is_locked: boolean;
  locked_until: Date | null;
}): Promise<{ locked: boolean; reason?: string }> {
  if (user.is_locked) {
    return {
      locked: true,
      reason: "Account has been locked by an administrator.",
    };
  }
  if (user.locked_until && new Date(user.locked_until) > new Date()) {
    const minutes = Math.ceil(
      (new Date(user.locked_until).getTime() - Date.now()) / 60000,
    );
    return {
      locked: true,
      reason: `Account is temporarily locked due to too many failed attempts. Try again in ${minutes} minute(s).`,
    };
  }
  return { locked: false };
}

// ── Session mapping ──────────────────────────────────────────────────────────

/**
 * Convert a raw database user row into a `SessionUser`.
 *
 * The database `role_name` is mapped to an application-level role slug:
 *   SUPER_ADMIN → super_admin
 *   ADMIN       → admin
 *   SALES_USER  → sales
 *   DEVELOPER   → dev
 *
 * If no `avatar_url` is present, an SVG data-URI with the user's initials is
 * generated inline as the fallback.
 *
 * @param dbUser - Raw row from `getUserBy*` queries.
 * @returns      A normalised `SessionUser`.
 */
export function mapDbUserToSessionUser(
  dbUser: Record<string, unknown>,
): SessionUser {
  const roleName = dbUser.role_name as string;

  const roleMapping: Record<string, string> = {
    SUPER_ADMIN: "super_admin",
    ADMIN: "admin",
    SALES_USER: "sales",
    DEVELOPER: "dev",
  };

  const avatarUrl = dbUser.avatar_url as string | null

  return {
    id: dbUser.id as string,
    username: dbUser.username as string,
    email: dbUser.email as string,
    phone: (dbUser.phone as string) || null,
    firstName: dbUser.first_name as string,
    lastName: dbUser.last_name as string,
    role: roleMapping[roleName] || roleName.toLowerCase(),
    avatar: avatarUrl || (() => { const f = ((dbUser.first_name as string)?.[0] || '').toUpperCase(); const l = ((dbUser.last_name as string)?.[0] || '').toUpperCase(); return `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><rect width="128" height="128" fill="#1d4ed8"/><text x="64" y="80" font-size="48" fill="white" text-anchor="middle" font-family="Arial,Helvetica,sans-serif">${f}${l}</text></svg>`)}` })(),
  };
}

// ── Session management ───────────────────────────────────────────────────────

/**
 * Create a signed JWT session token for a given user.
 *
 * The token embeds `userId` and `role` and expires after 24 hours.
 *
 * @param userId - The user's UUID.
 * @param role   - The user's role slug.
 * @returns      A signed JWT string.
 */
export async function createSession(userId: string, role: string) {
  return signToken({ userId, role });
}

/**
 * Encrypt a plaintext password using the `encrypt_password` PL/pgSQL function.
 *
 * This is used for stored secrets that need to be decrypted later (e.g.
 * third-party API keys), not for login credentials.
 *
 * @param password - Plaintext to encrypt.
 * @returns        The encrypted text.
 */
export async function encryptPassword(password: string): Promise<string> {
  const result = await query("SELECT encrypt_password($1) AS encrypted", [password]);
  return result.rows[0]?.encrypted;
}

/**
 * Decrypt a value previously encrypted by `encryptPassword`.
 *
 * Calls the `decrypt_password` PL/pgSQL function.  Returns `null` on any
 * error (bad ciphertext, missing key, etc.).
 *
 * @param encrypted - Previously encrypted text.
 * @returns         The original plaintext or `null`.
 */
export async function decryptPassword(encrypted: string): Promise<string | null> {
  try {
    const result = await query("SELECT decrypt_password($1) AS decrypted", [encrypted]);
    return result.rows[0]?.decrypted || null;
  } catch {
    return null;
  }
}

/**
 * Push context values into the Postgres session so that triggers and RLS
 * policies can identify the calling user.
 *
 * @param userId - The user's UUID.
 * @param ip     - Optional client IP (stored in `app.current_ip`).
 */
export async function setSessionContext(userId: string, ip?: string) {
  await query("SELECT set_session_user_context($1)", [userId]);
  if (ip) {
    await query("SELECT set_config('app.current_ip', $1, true)", [ip]);
  }
}

/**
 * Restore the `SessionUser` from the current request's session cookie.
 *
 * Reads the cookie named `"session"`, verifies the JWT, fetches the user from
 * the database, and maps the result to a `SessionUser`.
 *
 * Returns `null` when:
 *   • No cookie is present
 *   • The token is expired / invalid
 *   • The user no longer exists
 *
 * @returns The authenticated `SessionUser` or `null`.
 */
export async function getSessionUser(): Promise<SessionUser | null> {
  try {
    const cookieStore = await cookies();
    const token = cookieStore.get(SESSION_COOKIE)?.value;
    if (!token) return null;

    const payload = await verifyToken(token);
    if (!payload) return null;

    const dbUser = await getUserById(payload.userId);
    if (!dbUser) return null;

    return mapDbUserToSessionUser(dbUser);
  } catch {
    return null;
  }
}
