import crypto from "node:crypto";

import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";

import { env } from "../config/env.js";
import { pool } from "../db/pool.js";
import { ApiError } from "../utils/ApiError.js";
import { logAudit } from "../utils/auditLog.js";
import {
  canManageAdmin,
  normalizePermissions,
  ROLE_LABELS,
} from "../utils/permissions.js";
import { ensureSystemSettings } from "../utils/systemSettings.js";
import { parseBrowserName } from "../utils/userAgent.js";
import { ensureMgxStaffMobileTables } from "./mgxStaffMobileController.js";

const cleanString = (value) => String(value ?? "").trim();

const SUPPORTED_MODULES = ["SCF", "MGX", "TRACKING"];

const normalizeModule = (value) => cleanString(value).toUpperCase();

const normalizeAllowedModules = (value) => {
  const source = Array.isArray(value) ? value : value ? [value] : ["SCF"];
  const expanded = source.some((item) => normalizeModule(item) === "ALL")
    ? SUPPORTED_MODULES
    : source;
  const modules = [
    ...new Set(expanded.map(normalizeModule).filter((module) => SUPPORTED_MODULES.includes(module))),
  ];

  return modules.length ? modules : ["SCF"];
};

const resolveActiveModule = (allowedModules, requestedModule = null, defaultModule = "SCF") => {
  const normalizedAllowed = normalizeAllowedModules(allowedModules);
  const requested = normalizeModule(requestedModule);

  if (normalizedAllowed.includes(requested)) {
    return requested;
  }

  const fallback = normalizeModule(defaultModule);

  if (normalizedAllowed.includes(fallback)) {
    return fallback;
  }

  return normalizedAllowed[0];
};

const buildSessionCookieOptions = (maxAge) => ({
  httpOnly: true,
  sameSite: "lax",
  secure: env.nodeEnv === "production",
  path: "/",
  maxAge,
});

const buildRefreshCookieOptions = (maxAge) => ({
  httpOnly: true,
  sameSite: "lax",
  secure: env.nodeEnv === "production",
  path: "/",
  maxAge,
});

const clearSessionCookieOptions = {
  httpOnly: true,
  sameSite: "lax",
  secure: env.nodeEnv === "production",
  path: "/",
};

const clearRefreshCookieOptions = {
  httpOnly: true,
  sameSite: "lax",
  secure: env.nodeEnv === "production",
  path: "/",
};

const hashRefreshToken = (token) =>
  crypto.createHash("sha256").update(String(token || "")).digest("hex");

const createRefreshTokenValue = () => crypto.randomBytes(48).toString("hex");

const SESSION_USER_SELECT = `
  SELECT
    u.id,
    u.email,
    u.password_hash,
    u.full_name,
    u.role,
    u.company_id,
    u.allowed_modules,
    u.default_module,
    u.is_active,
    u.last_login,
    u.failed_login_attempts,
    u.locked_at,
    r.permissions
  FROM users u
  LEFT JOIN roles r
    ON r.name = u.role
`;

const MOBILE_ROLE_TO_AUTH_ROLE = {
  chauffeur: "mgx_chauffeur",
  chef_parc: "mgx_chef_parc",
  mecanicien: "mgx_mecanicien",
  back_office: "mgx_back_office",
  controleur_mgx: "mgx_controleur",
};

const MOBILE_ACCOUNT_SELECT = `
  SELECT
    ma.id,
    ma.company_id,
    ma.role_type,
    ma.driver_id,
    ma.staff_profile_id,
    ma.email,
    ma.password_hash,
    ma.is_active,
    ma.last_login,
    ma.failed_login_attempts,
    ma.locked_at,
    c.name AS company_name,
    c.currency AS company_currency,
    dr.full_name AS driver_full_name,
    dr.phone AS driver_phone,
    dr.depot_id AS driver_depot_id,
    dd.label AS driver_depot_label,
    sp.full_name AS staff_full_name,
    sp.phone AS staff_phone,
    sp.depot_id AS staff_depot_id,
    sd.label AS staff_depot_label
  FROM mgx_mobile_accounts ma
  JOIN companies c ON c.id = ma.company_id
  LEFT JOIN mgx_drivers dr ON dr.id = ma.driver_id
  LEFT JOIN mgx_depots dd ON dd.id = dr.depot_id
  LEFT JOIN mgx_staff_profiles sp ON sp.id = ma.staff_profile_id
  LEFT JOIN mgx_depots sd ON sd.id = sp.depot_id
`;

const getUserSessionByEmail = async (db, email) => {
  const { rows } = await db.query(
    `${SESSION_USER_SELECT} WHERE LOWER(u.email) = LOWER($1) LIMIT 1`,
    [email],
  );

  return rows[0] || null;
};

const getUserSessionById = async (db, userId) => {
  const { rows } = await db.query(`${SESSION_USER_SELECT} WHERE u.id = $1 LIMIT 1`, [
    userId,
  ]);

  return rows[0] || null;
};

const getMobileAccountSessionByEmail = async (db, email) => {
  const { rows } = await db.query(
    `${MOBILE_ACCOUNT_SELECT} WHERE LOWER(ma.email) = LOWER($1) LIMIT 1`,
    [email],
  );

  return rows[0] || null;
};

const getMobileAccountSessionById = async (db, mobileAccountId) => {
  const { rows } = await db.query(
    `${MOBILE_ACCOUNT_SELECT} WHERE ma.id = $1 LIMIT 1`,
    [mobileAccountId],
  );

  return rows[0] || null;
};

const fetchAuthorizedCompanies = async (
  db,
  userId,
  fallbackCompanyId = null,
  role = null,
  permissions = [],
) => {
  if (canManageAdmin(role, permissions)) {
    const { rows } = await db.query(
      `
        SELECT id, name, currency
        FROM companies
        ORDER BY name ASC
      `,
    );

    return rows.map((row) => ({
      id: Number(row.id),
      name: row.name,
      currency: row.currency,
    }));
  }

  const { rows } = await db.query(
    `
      SELECT DISTINCT c.id, c.name, c.currency
      FROM companies c
      LEFT JOIN user_companies uc
        ON uc.company_id = c.id
      WHERE uc.user_id = $1
         OR c.id = $2
      ORDER BY c.name ASC
    `,
    [userId, fallbackCompanyId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    name: row.name,
    currency: row.currency,
  }));
};

const fetchCompanyContext = async (db, companyId) => {
  if (!companyId) {
    return {
      company: null,
      fiscalYear: null,
      period: null,
    };
  }

  const { rows } = await db.query(
    `
      SELECT
        c.id AS company_id,
        c.name AS company_name,
        c.currency AS company_currency,
        fy.id AS fiscal_year_id,
        fy.label AS fiscal_year_label,
        fy.is_closed AS fiscal_year_is_closed,
        ap.id AS period_id,
        ap.month AS period_month,
        ap.year AS period_year,
        ap.is_closed AS period_is_closed
      FROM companies c
      LEFT JOIN LATERAL (
        SELECT id, label, is_closed
        FROM fiscal_years
        WHERE company_id = c.id
        ORDER BY is_current DESC, start_date DESC
        LIMIT 1
      ) AS fy
        ON TRUE
      LEFT JOIN LATERAL (
        SELECT id, month, year, is_closed
        FROM accounting_periods
        WHERE company_id = c.id
          AND (fy.id IS NULL OR fiscal_year_id = fy.id)
        ORDER BY
          CASE
            WHEN year = EXTRACT(YEAR FROM CURRENT_DATE)::int
              AND month = EXTRACT(MONTH FROM CURRENT_DATE)::int
              THEN 0
            ELSE 1
          END,
          year DESC,
          month DESC
        LIMIT 1
      ) AS ap
        ON TRUE
      WHERE c.id = $1
      LIMIT 1
    `,
    [companyId],
  );

  const row = rows[0];

  if (!row) {
    return {
      company: null,
      fiscalYear: null,
      period: null,
    };
  }

  return {
    company: {
      id: Number(row.company_id),
      name: row.company_name,
      currency: row.company_currency,
    },
    fiscalYear: row.fiscal_year_id
      ? {
          id: Number(row.fiscal_year_id),
          label: row.fiscal_year_label,
          isClosed: row.fiscal_year_is_closed,
        }
      : null,
    period: row.period_id
      ? {
          id: Number(row.period_id),
          month: Number(row.period_month),
          year: Number(row.period_year),
          isClosed: row.period_is_closed,
          label: `${String(row.period_month).padStart(2, "0")}/${row.period_year}`,
        }
      : null,
  };
};

const buildUserPayload = ({ row, companies, context, activeCompanyId, activeModule }) => {
  const allowedModules = normalizeAllowedModules(row.allowed_modules);

  return {
    id: row.id,
    email: row.email,
    fullName: row.full_name,
    role: row.role,
    roleLabel: ROLE_LABELS[row.role] || row.role,
    permissions: normalizePermissions(row.permissions),
    isActive: row.is_active,
    companyId: activeCompanyId,
    allowed_modules: allowedModules,
    allowedModules,
    default_module: row.default_module || "SCF",
    defaultModule: row.default_module || "SCF",
    active_module: resolveActiveModule(allowedModules, activeModule, row.default_module),
    activeModule: resolveActiveModule(allowedModules, activeModule, row.default_module),
    companies,
    company: context.company,
    fiscalYear: context.fiscalYear,
    period: context.period,
    lastLogin: row.last_login ? new Date(row.last_login).toISOString() : null,
  };
};

const buildMobileUserPayload = ({ row, context }) => {
  const linkedFullName = row.driver_full_name || row.staff_full_name || row.email;
  const linkedPhone = row.driver_phone || row.staff_phone || null;
  const depotId = row.driver_depot_id || row.staff_depot_id || null;
  const depotName = row.driver_depot_label || row.staff_depot_label || null;
  const role = MOBILE_ROLE_TO_AUTH_ROLE[row.role_type] || row.role_type;
  const companyId = Number(row.company_id);

  return {
    id: Number(row.id),
    email: row.email,
    username: row.email,
    fullName: linkedFullName,
    full_name: linkedFullName,
    role,
    role_type: row.role_type,
    roleLabel: ROLE_LABELS[role] || row.role_type,
    permissions: [],
    isActive: row.is_active,
    is_active: row.is_active,
    companyId,
    allowed_modules: ["MGX"],
    allowedModules: ["MGX"],
    default_module: "MGX",
    defaultModule: "MGX",
    active_module: "MGX",
    activeModule: "MGX",
    companies: [
      {
        id: companyId,
        name: row.company_name,
        currency: row.company_currency,
      },
    ],
    company: context.company,
    fiscalYear: context.fiscalYear,
    period: context.period,
    lastLogin: row.last_login ? new Date(row.last_login).toISOString() : null,
    phone: linkedPhone,
    depotId: depotId ? Number(depotId) : null,
    depotName,
    depot: depotId ? { id: Number(depotId), label: depotName } : null,
    mobile_account_id: Number(row.id),
    mobileAccountId: Number(row.id),
    linked_entity_type: row.driver_id ? "driver" : "staff_profile",
    linked_entity_id: row.driver_id ? Number(row.driver_id) : row.staff_profile_id ? Number(row.staff_profile_id) : null,
  };
};

const signToken = ({ row, companyIds, activeCompanyId, sessionExpirationMinutes, module }) =>
  jwt.sign(
    {
      sub: row.id,
      email: row.email,
      fullName: row.full_name,
      role: row.role,
      companyId: activeCompanyId,
      companyIds,
      module: resolveActiveModule(row.allowed_modules, module, row.default_module),
    },
    env.jwtSecret,
    {
      expiresIn: `${sessionExpirationMinutes}m`,
    },
  );

const signMobileToken = ({ row, sessionExpirationMinutes }) =>
  jwt.sign(
    {
      sub: Number(row.id),
      email: row.email,
      role: MOBILE_ROLE_TO_AUTH_ROLE[row.role_type] || row.role_type,
      role_type: row.role_type,
      companyId: Number(row.company_id),
      companyIds: [Number(row.company_id)],
      module: "MGX",
      accountType: "mgx_mobile_account",
      linkedEntityId: row.driver_id ? Number(row.driver_id) : row.staff_profile_id ? Number(row.staff_profile_id) : null,
      linkedEntityType: row.driver_id ? "driver" : "staff_profile",
    },
    env.jwtSecret,
    {
      expiresIn: `${sessionExpirationMinutes}m`,
    },
  );

const verifyMobileQrLoginToken = (token) => {
  const payload = jwt.verify(String(token || ""), env.jwtSecret);
  if (payload?.purpose !== "mgx_mobile_qr_login" || !payload?.mobileAccountId) {
    throw new ApiError(401, "QR de connexion invalide.");
  }
  return payload;
};

const createRefreshToken = async (db, { userId, req, ttlDays }) => {
  const rawToken = createRefreshTokenValue();
  const expiresAt = new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000);
  const { rows } = await db.query(
    `
      INSERT INTO refresh_tokens (
        user_id,
        token_hash,
        expires_at,
        ip_address,
        user_agent
      )
      VALUES ($1, $2, $3, $4, $5)
      RETURNING id
    `,
    [
      userId,
      hashRefreshToken(rawToken),
      expiresAt.toISOString(),
      req.ip,
      req.headers["user-agent"] || null,
    ],
  );

  return {
    id: Number(rows[0].id),
    value: rawToken,
    expiresAt,
  };
};

const getActiveRefreshToken = async (db, rawToken) => {
  if (!rawToken) {
    return null;
  }

  const { rows } = await db.query(
    `
      SELECT id, user_id, expires_at, revoked_at
      FROM refresh_tokens
      WHERE token_hash = $1
      LIMIT 1
    `,
    [hashRefreshToken(rawToken)],
  );

  const row = rows[0];

  if (!row) {
    return null;
  }

  const isExpired = new Date(row.expires_at).getTime() <= Date.now();

  if (row.revoked_at || isExpired) {
    return null;
  }

  return {
    id: Number(row.id),
    user_id: Number(row.user_id),
    expires_at: row.expires_at,
  };
};

const revokeRefreshTokenById = async (db, tokenId, replacementTokenId = null) => {
  if (!tokenId) {
    return;
  }

  await db.query(
    `
      UPDATE refresh_tokens
      SET
        revoked_at = CURRENT_TIMESTAMP,
        replaced_by_token_id = COALESCE($2, replaced_by_token_id)
      WHERE id = $1
    `,
    [tokenId, replacementTokenId],
  );
};

const revokeRefreshTokenByValue = async (db, rawToken) => {
  if (!rawToken) {
    return;
  }

  await db.query(
    `
      UPDATE refresh_tokens
      SET revoked_at = CURRENT_TIMESTAMP
      WHERE token_hash = $1
        AND revoked_at IS NULL
    `,
    [hashRefreshToken(rawToken)],
  );
};

const recordLoginHistory = async (
  db,
  { companyId = null, userId = null, email = null, result, reason = null, req },
) => {
  await db.query(
    `
      INSERT INTO login_history (
        company_id,
        user_id,
        email,
        ip_address,
        user_agent,
        browser,
        result,
        reason
      )
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
    `,
    [
      companyId,
      userId,
      email,
      req.ip,
      req.headers["user-agent"] || null,
      parseBrowserName(req.headers["user-agent"]),
      result,
      reason,
    ],
  );
};

const resolveActiveCompanyId = (companies, requestedCompanyId, fallbackCompanyId) => {
  const authorizedIds = companies.map((company) => Number(company.id));
  const requested = Number(requestedCompanyId || 0);

  if (requested && authorizedIds.includes(requested)) {
    return requested;
  }

  const fallback = Number(fallbackCompanyId || 0);

  if (fallback && authorizedIds.includes(fallback)) {
    return fallback;
  }

  return authorizedIds[0] || null;
};

const buildAuthenticatedUser = async (
  db,
  row,
  requestedCompanyId = null,
  requestedModule = null,
) => {
  const permissions = normalizePermissions(row.permissions);
  const companies = await fetchAuthorizedCompanies(
    db,
    row.id,
    row.company_id,
    row.role,
    permissions,
  );
  const activeCompanyId = resolveActiveCompanyId(companies, requestedCompanyId, row.company_id);
  const context = await fetchCompanyContext(db, activeCompanyId);
  const activeModule = resolveActiveModule(row.allowed_modules, requestedModule, row.default_module);

  return {
    activeCompanyId,
    activeModule,
    companies,
    context,
    user: buildUserPayload({
      row,
      companies,
      context,
      activeCompanyId,
      activeModule,
    }),
  };
};

const buildAuthenticatedMobileUser = async (db, row) => {
  const context = await fetchCompanyContext(db, Number(row.company_id));

  return {
    activeCompanyId: Number(row.company_id),
    activeModule: "MGX",
    companies: [
      {
        id: Number(row.company_id),
        name: row.company_name,
        currency: row.company_currency,
      },
    ],
    context,
    user: buildMobileUserPayload({ row, context }),
  };
};

export const authController = {
  login: async (req, res) => {
    const email = cleanString(req.body.email);
    const password = String(req.body.password || "");

    if (!email || !password) {
      throw new ApiError(400, "Email and password are required.");
    }

    const client = await pool.connect();
    let transactionCompleted = false;

    try {
      await client.query("BEGIN");

      const settings = await ensureSystemSettings(client);
      const sessionRow = await getUserSessionByEmail(client, email);

      if (!sessionRow) {
        await recordLoginHistory(client, {
          email,
          result: "failure",
          reason: "unknown_email",
          req,
        });
        await client.query("COMMIT");
        transactionCompleted = true;
        throw new ApiError(401, "Adresse e-mail ou mot de passe invalide.");
      }

      if (!sessionRow.is_active) {
        await recordLoginHistory(client, {
          companyId: sessionRow.company_id,
          userId: sessionRow.id,
          email: sessionRow.email,
          result: "failure",
          reason: "inactive_account",
          req,
        });
        await client.query("COMMIT");
        transactionCompleted = true;
        throw new ApiError(403, "Votre compte est désactivé.");
      }

      if (sessionRow.locked_at) {
        await recordLoginHistory(client, {
          companyId: sessionRow.company_id,
          userId: sessionRow.id,
          email: sessionRow.email,
          result: "failure",
          reason: "locked_account",
          req,
        });
        await client.query("COMMIT");
        transactionCompleted = true;
        throw new ApiError(
          423,
          "Votre compte est verrouillé après plusieurs tentatives échouées.",
        );
      }

      const passwordMatches = await bcrypt.compare(password, sessionRow.password_hash);

      if (!passwordMatches) {
        const nextFailedAttempts = Number(sessionRow.failed_login_attempts || 0) + 1;
        const shouldLock = nextFailedAttempts >= settings.max_failed_logins_before_lockout;

        await client.query(
          `
            UPDATE users
            SET
              failed_login_attempts = $2,
              locked_at = CASE WHEN $3 THEN CURRENT_TIMESTAMP ELSE locked_at END
            WHERE id = $1
          `,
          [sessionRow.id, nextFailedAttempts, shouldLock],
        );

        await recordLoginHistory(client, {
          companyId: sessionRow.company_id,
          userId: sessionRow.id,
          email: sessionRow.email,
          result: "failure",
          reason: shouldLock ? "locked_after_failures" : "invalid_password",
          req,
        });

        await client.query("COMMIT");
        transactionCompleted = true;

        if (shouldLock) {
          throw new ApiError(
            423,
            "Votre compte est verrouillé après plusieurs tentatives échouées.",
          );
        }

        throw new ApiError(401, "Adresse e-mail ou mot de passe invalide.");
      }

      await client.query(
        `
          UPDATE users
          SET
            last_login = CURRENT_TIMESTAMP,
            failed_login_attempts = 0,
            locked_at = NULL
          WHERE id = $1
        `,
        [sessionRow.id],
      );

      await recordLoginHistory(client, {
        companyId: sessionRow.company_id,
        userId: sessionRow.id,
        email: sessionRow.email,
        result: "success",
        reason: "login_success",
        req,
      });

      const refreshedSessionRow = await getUserSessionById(client, sessionRow.id);
      const authenticatedUser = await buildAuthenticatedUser(client, refreshedSessionRow);
      const refreshToken = await createRefreshToken(client, {
        userId: sessionRow.id,
        req,
        ttlDays: env.refreshTokenTtlDays,
      });

      await logAudit(client, {
        companyId: authenticatedUser.activeCompanyId,
        userId: sessionRow.id,
        action: "auth.login",
        entityType: "user",
        entityId: sessionRow.id,
        newData: {
          email: sessionRow.email,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");
      transactionCompleted = true;

      const token = signToken({
        row: refreshedSessionRow,
        companyIds: authenticatedUser.companies.map((company) => company.id),
        activeCompanyId: authenticatedUser.activeCompanyId,
        sessionExpirationMinutes: settings.session_expiration_minutes,
        module: authenticatedUser.activeModule,
      });

      res.cookie(
        env.jwtCookieName,
        token,
        buildSessionCookieOptions(settings.session_expiration_minutes * 60 * 1000),
      );
      res.cookie(
        env.jwtRefreshCookieName,
        refreshToken.value,
        buildRefreshCookieOptions(env.refreshTokenTtlDays * 24 * 60 * 60 * 1000),
      );

      res.json({
        message: "Authentification réussie.",
        token,
        allowed_modules: authenticatedUser.user.allowed_modules,
        default_module: authenticatedUser.user.default_module,
        user: authenticatedUser.user,
      });
    } catch (error) {
      if (!transactionCompleted) {
        await client.query("ROLLBACK");
      }
      throw error;
    } finally {
      client.release();
    }
  },

  mobileLogin: async (req, res) => {
    const email = cleanString(req.body.email);
    const password = String(req.body.password || "");

    if (!email || !password) {
      throw new ApiError(400, "Email and password are required.");
    }

    await ensureMgxStaffMobileTables();
    const client = await pool.connect();
    let transactionCompleted = false;

    try {
      await client.query("BEGIN");

      const settings = await ensureSystemSettings(client);
      const sessionRow = await getMobileAccountSessionByEmail(client, email);

      if (!sessionRow) {
        await recordLoginHistory(client, {
          email,
          result: "failure",
          reason: "unknown_mobile_email",
          req,
        });
        await client.query("COMMIT");
        transactionCompleted = true;
        throw new ApiError(401, "Adresse e-mail ou mot de passe invalide.");
      }

      if (!sessionRow.is_active) {
        await recordLoginHistory(client, {
          companyId: sessionRow.company_id,
          email: sessionRow.email,
          result: "failure",
          reason: "inactive_mobile_account",
          req,
        });
        await client.query("COMMIT");
        transactionCompleted = true;
        throw new ApiError(403, "Votre compte mobile est désactivé.");
      }

      if (sessionRow.locked_at) {
        await recordLoginHistory(client, {
          companyId: sessionRow.company_id,
          email: sessionRow.email,
          result: "failure",
          reason: "locked_mobile_account",
          req,
        });
        await client.query("COMMIT");
        transactionCompleted = true;
        throw new ApiError(423, "Votre compte mobile est verrouillé.");
      }

      const passwordMatches = await bcrypt.compare(password, sessionRow.password_hash);

      if (!passwordMatches) {
        const nextFailedAttempts = Number(sessionRow.failed_login_attempts || 0) + 1;
        const shouldLock = nextFailedAttempts >= settings.max_failed_logins_before_lockout;

        await client.query(
          `
            UPDATE mgx_mobile_accounts
            SET
              failed_login_attempts = $2,
              locked_at = CASE WHEN $3 THEN CURRENT_TIMESTAMP ELSE locked_at END
            WHERE id = $1
          `,
          [sessionRow.id, nextFailedAttempts, shouldLock],
        );

        await recordLoginHistory(client, {
          companyId: sessionRow.company_id,
          email: sessionRow.email,
          result: "failure",
          reason: shouldLock ? "locked_mobile_after_failures" : "invalid_mobile_password",
          req,
        });

        await client.query("COMMIT");
        transactionCompleted = true;

        if (shouldLock) {
          throw new ApiError(423, "Votre compte mobile est verrouillé.");
        }

        throw new ApiError(401, "Adresse e-mail ou mot de passe invalide.");
      }

      await client.query(
        `
          UPDATE mgx_mobile_accounts
          SET
            last_login = CURRENT_TIMESTAMP,
            failed_login_attempts = 0,
            locked_at = NULL
          WHERE id = $1
        `,
        [sessionRow.id],
      );

      await recordLoginHistory(client, {
        companyId: sessionRow.company_id,
        email: sessionRow.email,
        result: "success",
        reason: "mobile_login_success",
        req,
      });

      const refreshedSessionRow = await getMobileAccountSessionById(client, sessionRow.id);
      const authenticatedUser = await buildAuthenticatedMobileUser(client, refreshedSessionRow);

      await logAudit(client, {
        companyId: Number(sessionRow.company_id),
        userId: null,
        action: "auth.mobile_login",
        entityType: "mgx_mobile_accounts",
        entityId: sessionRow.id,
        newData: {
          email: sessionRow.email,
          role_type: sessionRow.role_type,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");
      transactionCompleted = true;

      const token = signMobileToken({
        row: refreshedSessionRow,
        sessionExpirationMinutes: settings.session_expiration_minutes,
      });

      res.json({
        message: "Authentification mobile réussie.",
        token,
        allowed_modules: authenticatedUser.user.allowed_modules,
        default_module: authenticatedUser.user.default_module,
        user: authenticatedUser.user,
      });
    } catch (error) {
      if (!transactionCompleted) {
        await client.query("ROLLBACK");
      }
      throw error;
    } finally {
      client.release();
    }
  },

  mobileQrLogin: async (req, res) => {
    const qrToken = String(req.body.qr_token || "");

    if (!qrToken) {
      throw new ApiError(400, "QR token requis.");
    }

    const client = await pool.connect();
    let transactionCompleted = false;

    try {
      await client.query("BEGIN");
      await ensureMgxStaffMobileTables();

      const settings = await ensureSystemSettings(client);
      const qrPayload = verifyMobileQrLoginToken(qrToken);
      const sessionRow = await getMobileAccountSessionById(client, Number(qrPayload.mobileAccountId));

      if (!sessionRow) {
        throw new ApiError(401, "Compte mobile introuvable.");
      }

      if (!sessionRow.is_active) {
        throw new ApiError(403, "Votre compte mobile est désactivé.");
      }

      if (sessionRow.locked_at) {
        throw new ApiError(423, "Votre compte mobile est verrouillé.");
      }

      await client.query(
        `
          UPDATE mgx_mobile_accounts
          SET
            last_login = CURRENT_TIMESTAMP,
            failed_login_attempts = 0,
            locked_at = NULL
          WHERE id = $1
        `,
        [sessionRow.id],
      );

      await recordLoginHistory(client, {
        companyId: sessionRow.company_id,
        email: sessionRow.email,
        result: "success",
        reason: "mobile_qr_login_success",
        req,
      });

      const refreshedSessionRow = await getMobileAccountSessionById(client, sessionRow.id);
      const authenticatedUser = await buildAuthenticatedMobileUser(client, refreshedSessionRow);

      await logAudit(client, {
        companyId: Number(sessionRow.company_id),
        userId: null,
        action: "auth.mobile_qr_login",
        entityType: "mgx_mobile_accounts",
        entityId: sessionRow.id,
        newData: {
          email: sessionRow.email,
          role_type: sessionRow.role_type,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");
      transactionCompleted = true;

      const token = signMobileToken({
        row: refreshedSessionRow,
        sessionExpirationMinutes: settings.session_expiration_minutes,
      });

      res.json({
        message: "Authentification mobile QR réussie.",
        token,
        allowed_modules: authenticatedUser.user.allowed_modules,
        default_module: authenticatedUser.user.default_module,
        user: authenticatedUser.user,
      });
    } catch (error) {
      if (!transactionCompleted) {
        await client.query("ROLLBACK");
      }
      if (error?.name === "TokenExpiredError" || error?.name === "JsonWebTokenError") {
        throw new ApiError(401, "QR de connexion expiré ou invalide.");
      }
      throw error;
    } finally {
      client.release();
    }
  },

  logout: async (req, res) => {
    const refreshToken = req.cookies?.[env.jwtRefreshCookieName];

    if (refreshToken) {
      try {
        await revokeRefreshTokenByValue(pool, refreshToken);
      } catch (error) {
        console.error("Refresh token revoke failed", error);
      }
    }

    if (req.user?.sub) {
      try {
        await logAudit(pool, {
          companyId: req.user.companyId || null,
          userId: req.user.sub,
          action: "auth.logout",
          entityType: "user",
          entityId: req.user.sub,
          ipAddress: req.ip,
        });
      } catch (error) {
        console.error("Logout audit log failed", error);
      }
    }

    res.clearCookie(env.jwtCookieName, clearSessionCookieOptions);
    res.clearCookie(env.jwtRefreshCookieName, clearRefreshCookieOptions);
    res.json({
      message: "Déconnexion réussie.",
    });
  },

  changePassword: async (req, res) => {
    const currentPassword = String(req.body.current_password || "");
    const newPassword = String(req.body.new_password || "");

    if (!/\d/.test(newPassword)) {
      throw new ApiError(400, "Le nouveau mot de passe doit contenir au moins un chiffre.");
    }

    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      if (req.user?.accountType === "mgx_mobile_account") {
        await ensureMgxStaffMobileTables();
        const sessionRow = await getMobileAccountSessionById(client, req.user.sub);

        if (!sessionRow || !sessionRow.is_active || sessionRow.locked_at) {
          throw new ApiError(401, "Session invalide.");
        }

        const matches = await bcrypt.compare(currentPassword, sessionRow.password_hash);

        if (!matches) {
          throw new ApiError(400, "Le mot de passe actuel est incorrect.");
        }

        const nextPasswordHash = await bcrypt.hash(newPassword, 12);

        await client.query(
          `
            UPDATE mgx_mobile_accounts
            SET password_hash = $2, failed_login_attempts = 0, locked_at = NULL, updated_at = NOW()
            WHERE id = $1
          `,
          [sessionRow.id, nextPasswordHash],
        );

        await logAudit(client, {
          companyId: req.user.companyId || null,
          userId: null,
          action: "auth.mobile_change_password",
          entityType: "mgx_mobile_accounts",
          entityId: sessionRow.id,
          ipAddress: req.ip,
        });
      } else {
        const sessionRow = await getUserSessionById(client, req.user.sub);

        if (!sessionRow || !sessionRow.is_active || sessionRow.locked_at) {
          throw new ApiError(401, "Session invalide.");
        }

        const matches = await bcrypt.compare(currentPassword, sessionRow.password_hash);

        if (!matches) {
          throw new ApiError(400, "Le mot de passe actuel est incorrect.");
        }

        const nextPasswordHash = await bcrypt.hash(newPassword, 12);

        await client.query(
          `
            UPDATE users
            SET password_hash = $2
            WHERE id = $1
          `,
          [sessionRow.id, nextPasswordHash],
        );

        await logAudit(client, {
          companyId: req.user.companyId || null,
          userId: sessionRow.id,
          action: "auth.change_password",
          entityType: "user",
          entityId: sessionRow.id,
          ipAddress: req.ip,
        });
      }

      await client.query("COMMIT");

      res.json({
        message: "Mot de passe mis à jour.",
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  refresh: async (req, res) => {
    const refreshCookie = req.cookies?.[env.jwtRefreshCookieName];

    if (!refreshCookie) {
      throw new ApiError(401, "Session invalide.");
    }

    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      const refreshTokenRow = await getActiveRefreshToken(client, refreshCookie);

      if (!refreshTokenRow) {
        throw new ApiError(401, "Session invalide.");
      }

      const sessionRow = await getUserSessionById(client, refreshTokenRow.user_id);

      if (!sessionRow || !sessionRow.is_active || sessionRow.locked_at) {
        await revokeRefreshTokenById(client, refreshTokenRow.id);
        throw new ApiError(401, "Session invalide.");
      }

      const settings = await ensureSystemSettings(client);
      const authenticatedUser = await buildAuthenticatedUser(
        client,
        sessionRow,
        null,
        req.body?.module || req.user?.module,
      );
      const nextRefreshToken = await createRefreshToken(client, {
        userId: sessionRow.id,
        req,
        ttlDays: env.refreshTokenTtlDays,
      });

      await revokeRefreshTokenById(client, refreshTokenRow.id, nextRefreshToken.id);

      await logAudit(client, {
        companyId: authenticatedUser.activeCompanyId,
        userId: sessionRow.id,
        action: "auth.refresh",
        entityType: "user",
        entityId: sessionRow.id,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      const accessToken = signToken({
        row: sessionRow,
        companyIds: authenticatedUser.companies.map((company) => company.id),
        activeCompanyId: authenticatedUser.activeCompanyId,
        sessionExpirationMinutes: settings.session_expiration_minutes,
        module: authenticatedUser.activeModule,
      });

      res.cookie(
        env.jwtCookieName,
        accessToken,
        buildSessionCookieOptions(settings.session_expiration_minutes * 60 * 1000),
      );
      res.cookie(
        env.jwtRefreshCookieName,
        nextRefreshToken.value,
        buildRefreshCookieOptions(env.refreshTokenTtlDays * 24 * 60 * 60 * 1000),
      );

      res.json({
        message: "Session renouvelée.",
        allowed_modules: authenticatedUser.user.allowed_modules,
        default_module: authenticatedUser.user.default_module,
        user: authenticatedUser.user,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      res.clearCookie(env.jwtCookieName, clearSessionCookieOptions);
      res.clearCookie(env.jwtRefreshCookieName, clearRefreshCookieOptions);
      throw error;
    } finally {
      client.release();
    }
  },

  setModule: async (req, res) => {
    const requestedModule = normalizeModule(req.body.module);

    if (!SUPPORTED_MODULES.includes(requestedModule)) {
      throw new ApiError(400, "Module invalide.");
    }

    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      const sessionRow = await getUserSessionById(client, req.user.sub);

      if (!sessionRow || !sessionRow.is_active || sessionRow.locked_at) {
        throw new ApiError(401, "Session invalide.");
      }

      const allowedModules = normalizeAllowedModules(sessionRow.allowed_modules);

      if (!allowedModules.includes(requestedModule)) {
        throw new ApiError(403, "Vous n'avez pas accès à ce module.");
      }

      if (req.body.set_default === true || req.body.setDefault === true) {
        await client.query(
          `
            UPDATE users
            SET default_module = $2
            WHERE id = $1
          `,
          [sessionRow.id, requestedModule],
        );
        sessionRow.default_module = requestedModule;
      }

      const settings = await ensureSystemSettings(client);
      const authenticatedUser = await buildAuthenticatedUser(
        client,
        sessionRow,
        req.body.company_id || req.user.companyId,
        requestedModule,
      );

      await logAudit(client, {
        companyId: authenticatedUser.activeCompanyId,
        userId: sessionRow.id,
        action: "auth.set_module",
        entityType: "user",
        entityId: sessionRow.id,
        newData: {
          module: requestedModule,
          set_default: req.body.set_default === true || req.body.setDefault === true,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      const accessToken = signToken({
        row: sessionRow,
        companyIds: authenticatedUser.companies.map((company) => company.id),
        activeCompanyId: authenticatedUser.activeCompanyId,
        sessionExpirationMinutes: settings.session_expiration_minutes,
        module: requestedModule,
      });

      res.cookie(
        env.jwtCookieName,
        accessToken,
        buildSessionCookieOptions(settings.session_expiration_minutes * 60 * 1000),
      );

      res.json({
        message: "Module actif mis à jour.",
        token: accessToken,
        allowed_modules: authenticatedUser.user.allowed_modules,
        default_module: authenticatedUser.user.default_module,
        user: authenticatedUser.user,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  me: async (req, res) => {
    const client = await pool.connect();

    try {
      const isMobileAccount = req.user?.accountType === "mgx_mobile_account";
      const sessionRow = isMobileAccount
        ? await getMobileAccountSessionById(client, req.user.sub)
        : await getUserSessionById(client, req.user.sub);

      if (!sessionRow || !sessionRow.is_active || sessionRow.locked_at) {
        throw new ApiError(401, "Session invalide.");
      }

      const authenticatedUser = isMobileAccount
        ? await buildAuthenticatedMobileUser(client, sessionRow)
        : await buildAuthenticatedUser(
            client,
            sessionRow,
            req.query.company_id,
            req.user?.module,
          );

      res.json({
        message: "Session active.",
        allowed_modules: authenticatedUser.user.allowed_modules,
        default_module: authenticatedUser.user.default_module,
        user: authenticatedUser.user,
      });
    } finally {
      client.release();
    }
  },

  mobileMe: async (req, res) => {
    const client = await pool.connect();

    try {
      if (req.user?.accountType !== "mgx_mobile_account") {
        throw new ApiError(401, "Session mobile invalide.");
      }

      await ensureMgxStaffMobileTables();
      const sessionRow = await getMobileAccountSessionById(client, req.user.sub);

      if (!sessionRow || !sessionRow.is_active || sessionRow.locked_at) {
        throw new ApiError(401, "Session mobile invalide.");
      }

      const authenticatedUser = await buildAuthenticatedMobileUser(client, sessionRow);

      res.json({
        message: "Session mobile active.",
        allowed_modules: authenticatedUser.user.allowed_modules,
        default_module: authenticatedUser.user.default_module,
        user: authenticatedUser.user,
      });
    } finally {
      client.release();
    }
  },
};
