import bcrypt from "bcryptjs";

import { pool } from "../db/pool.js";
import { ApiError } from "../utils/ApiError.js";
import { logAudit } from "../utils/auditLog.js";
import {
  buildPermissionGridState,
  canManageAdmin,
  fetchRolePermissions,
  getVisibleRoles,
  normalizePermissions,
  ROLE_LABELS,
  syncDefaultRoles,
} from "../utils/permissions.js";
import { ensureSystemSettings } from "../utils/systemSettings.js";

const PASSWORD_SALT_ROUNDS = 12;

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

const normalizeEmail = (value) => cleanString(value).toLowerCase();

const parseCompanyIds = (value) => {
  if (!value) {
    return [];
  }

  const source = Array.isArray(value) ? value : [value];

  return [...new Set(source.map((item) => Number(item)).filter((item) => Number.isFinite(item) && item > 0))];
};

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

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

const parseAllowedModules = (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 normalizeDefaultModule = (value, allowedModules) => {
  const normalized = normalizeModule(value || allowedModules[0] || "SCF");

  if (normalized === "BOTH") {
    return "BOTH";
  }

  return allowedModules.includes(normalized) ? normalized : allowedModules[0];
};

const ensureAdminPermission = async (db, req) => {
  await syncDefaultRoles(db);
  const permissions = await fetchRolePermissions(db, req.user?.role);

  if (!canManageAdmin(req.user?.role, permissions)) {
    throw new ApiError(403, "Accès refusé au module administration.");
  }

  return permissions;
};

const fetchAvailableCompanies = async (db) => {
  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 fetchRoleOptions = async (db) => {
  const { rows } = await db.query(
    `
      SELECT name, permissions
      FROM roles
      ORDER BY name ASC
    `,
  );

  return rows
    .map((row) => ({
      name: row.name,
      label: ROLE_LABELS[row.name] || row.name,
      permissions: normalizePermissions(row.permissions),
      permission_grid: buildPermissionGridState(row.permissions),
      visible: getVisibleRoles().some((role) => role.name === row.name),
    }))
    .sort((left, right) => {
      if (left.visible !== right.visible) {
        return left.visible ? -1 : 1;
      }

      return left.label.localeCompare(right.label, "fr");
    });
};

const mapUserRow = (row) => {
  const permissions = normalizePermissions(row.permissions);
  const companies = Array.isArray(row.authorized_companies) ? row.authorized_companies : [];

  return {
    id: Number(row.id),
    full_name: row.full_name,
    email: row.email,
    role: row.role,
    role_label: ROLE_LABELS[row.role] || row.role,
    allowed_modules: parseAllowedModules(row.allowed_modules),
    default_module: row.default_module || "SCF",
    company_id: row.company_id ? Number(row.company_id) : null,
    company_name: row.company_name || null,
    company_ids: companies.map((company) => Number(company.id)),
    companies: companies.map((company) => ({
      id: Number(company.id),
      name: company.name,
      currency: company.currency || null,
    })),
    is_active: row.is_active,
    failed_login_attempts: Number(row.failed_login_attempts || 0),
    locked_at: row.locked_at ? new Date(row.locked_at).toISOString() : null,
    last_login: row.last_login ? new Date(row.last_login).toISOString() : null,
    created_at: row.created_at ? new Date(row.created_at).toISOString() : null,
    permissions,
    permission_grid: buildPermissionGridState(permissions),
    status_label: row.locked_at
      ? "Verrouillé"
      : row.is_active
        ? "Actif"
        : "Inactif",
  };
};

const fetchUserById = async (db, userId) => {
  const { rows } = await db.query(
    `
      SELECT
        u.id,
        u.full_name,
        u.email,
        u.role,
        u.allowed_modules,
        u.default_module,
        u.company_id,
        u.is_active,
        u.last_login,
        u.created_at,
        u.failed_login_attempts,
        u.locked_at,
        r.permissions,
        c.name AS company_name,
        COALESCE(auth_companies.items, '[]'::json) AS authorized_companies
      FROM users u
      LEFT JOIN roles r
        ON r.name = u.role
      LEFT JOIN companies c
        ON c.id = u.company_id
      LEFT JOIN LATERAL (
        SELECT COALESCE(
          json_agg(
            json_build_object(
              'id', company_source.id,
              'name', company_source.name,
              'currency', company_source.currency
            )
            ORDER BY company_source.name
          ),
          '[]'::json
        ) AS items
        FROM (
          SELECT DISTINCT c2.id, c2.name, c2.currency
          FROM user_companies uc
          JOIN companies c2
            ON c2.id = uc.company_id
          WHERE uc.user_id = u.id

          UNION

          SELECT c3.id, c3.name, c3.currency
          FROM companies c3
          WHERE c3.id = u.company_id
        ) AS company_source
      ) AS auth_companies
        ON TRUE
      WHERE u.id = $1
      LIMIT 1
    `,
    [userId],
  );

  return rows[0] ? mapUserRow(rows[0]) : null;
};

const fetchUsers = async (db) => {
  const { rows } = await db.query(
    `
      SELECT
        u.id,
        u.full_name,
        u.email,
        u.role,
        u.allowed_modules,
        u.default_module,
        u.company_id,
        u.is_active,
        u.last_login,
        u.created_at,
        u.failed_login_attempts,
        u.locked_at,
        r.permissions,
        c.name AS company_name,
        COALESCE(auth_companies.items, '[]'::json) AS authorized_companies
      FROM users u
      LEFT JOIN roles r
        ON r.name = u.role
      LEFT JOIN companies c
        ON c.id = u.company_id
      LEFT JOIN LATERAL (
        SELECT COALESCE(
          json_agg(
            json_build_object(
              'id', company_source.id,
              'name', company_source.name,
              'currency', company_source.currency
            )
            ORDER BY company_source.name
          ),
          '[]'::json
        ) AS items
        FROM (
          SELECT DISTINCT c2.id, c2.name, c2.currency
          FROM user_companies uc
          JOIN companies c2
            ON c2.id = uc.company_id
          WHERE uc.user_id = u.id

          UNION

          SELECT c3.id, c3.name, c3.currency
          FROM companies c3
          WHERE c3.id = u.company_id
        ) AS company_source
      ) AS auth_companies
        ON TRUE
      ORDER BY u.full_name ASC, u.email ASC
    `,
  );

  return rows.map(mapUserRow);
};

const ensureRoleExists = async (db, roleName) => {
  const { rows } = await db.query(
    `
      SELECT name
      FROM roles
      WHERE name = $1
      LIMIT 1
    `,
    [roleName],
  );

  if (!rows[0]) {
    throw new ApiError(400, "Le rôle sélectionné est invalide.");
  }
};

const ensureCompaniesExist = async (db, companyIds) => {
  const { rows } = await db.query(
    `
      SELECT id
      FROM companies
      WHERE id = ANY($1::bigint[])
    `,
    [companyIds],
  );

  if (rows.length !== companyIds.length) {
    throw new ApiError(400, "Une ou plusieurs sociétés autorisées sont invalides.");
  }
};

const ensureUniqueEmail = async (db, email, excludeUserId = null) => {
  const { rows } = await db.query(
    `
      SELECT id
      FROM users
      WHERE LOWER(email) = LOWER($1)
        AND ($2::bigint IS NULL OR id <> $2)
      LIMIT 1
    `,
    [email, excludeUserId],
  );

  if (rows[0]) {
    throw new ApiError(409, "Cette adresse e-mail est déjà utilisée.");
  }
};

const syncUserCompanies = async (db, userId, companyIds) => {
  await db.query("DELETE FROM user_companies WHERE user_id = $1", [userId]);

  for (const companyId of companyIds) {
    await db.query(
      `
        INSERT INTO user_companies (user_id, company_id)
        VALUES ($1, $2)
        ON CONFLICT (user_id, company_id) DO NOTHING
      `,
      [userId, companyId],
    );
  }
};

export const usersController = {
  registerPushToken: async (req, res) => {
    const isMobileAccount = req.user?.accountType === "mgx_mobile_account";
    const userId = isMobileAccount ? null : Number(req.user?.sub || 0);
    const mobileAccountId = isMobileAccount
      ? Number(req.user?.mobileAccountId || req.user?.mobile_account_id || req.user?.sub || 0)
      : null;
    const pushToken = cleanString(req.body.push_token);
    const platform = cleanString(req.body.platform || "unknown").toLowerCase();

    if ((!userId && !mobileAccountId) || !pushToken) {
      throw new ApiError(400, "Token push invalide.");
    }

    if (isMobileAccount) {
      await pool.query(
        `
          CREATE TABLE IF NOT EXISTS mgx_mobile_push_tokens (
            id BIGSERIAL PRIMARY KEY,
            mobile_account_id BIGINT NOT NULL REFERENCES mgx_mobile_accounts (id) ON DELETE CASCADE,
            push_token TEXT NOT NULL,
            platform VARCHAR(30),
            is_active BOOLEAN NOT NULL DEFAULT TRUE,
            last_seen_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
            created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
            CONSTRAINT uq_mgx_mobile_push_tokens_token UNIQUE (push_token)
          )
        `,
      );

      const { rows } = await pool.query(
        `
          INSERT INTO mgx_mobile_push_tokens (
            mobile_account_id,
            push_token,
            platform,
            is_active,
            last_seen_at
          )
          VALUES ($1, $2, $3, TRUE, CURRENT_TIMESTAMP)
          ON CONFLICT (push_token)
          DO UPDATE SET
            mobile_account_id = EXCLUDED.mobile_account_id,
            platform = EXCLUDED.platform,
            is_active = TRUE,
            last_seen_at = CURRENT_TIMESTAMP
          RETURNING id, mobile_account_id, platform, last_seen_at
        `,
        [mobileAccountId, pushToken, platform],
      );

      res.json({
        item: {
          id: Number(rows[0].id),
          mobile_account_id: Number(rows[0].mobile_account_id),
          platform: rows[0].platform,
          last_seen_at: rows[0].last_seen_at,
        },
      });
      return;
    }

    await pool.query(
      `
        CREATE TABLE IF NOT EXISTS user_push_tokens (
          id BIGSERIAL PRIMARY KEY,
          user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
          push_token TEXT NOT NULL,
          platform VARCHAR(30),
          is_active BOOLEAN NOT NULL DEFAULT TRUE,
          last_seen_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
          created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
          CONSTRAINT uq_user_push_tokens_token UNIQUE (push_token)
        )
      `,
    );

    const { rows } = await pool.query(
      `
        INSERT INTO user_push_tokens (
          user_id,
          push_token,
          platform,
          is_active,
          last_seen_at
        )
        VALUES ($1, $2, $3, TRUE, CURRENT_TIMESTAMP)
        ON CONFLICT (push_token)
        DO UPDATE SET
          user_id = EXCLUDED.user_id,
          platform = EXCLUDED.platform,
          is_active = TRUE,
          last_seen_at = CURRENT_TIMESTAMP
        RETURNING id, user_id, platform, last_seen_at
      `,
      [userId, pushToken, platform],
    );

    res.json({
      item: {
        id: Number(rows[0].id),
        user_id: Number(rows[0].user_id),
        platform: rows[0].platform,
        last_seen_at: rows[0].last_seen_at,
      },
    });
  },

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

    try {
      await client.query("BEGIN");
      await ensureAdminPermission(client, req);

      const items = await fetchUsers(client);
      const roles = await fetchRoleOptions(client);
      const companies = await fetchAvailableCompanies(client);
      const settings = await ensureSystemSettings(client);

      await client.query("COMMIT");

      res.json({
        items,
        roles,
        companies,
        password_policy: {
          minimum_length: settings.password_minimum_length,
        },
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

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

    try {
      await client.query("BEGIN");
      await ensureAdminPermission(client, req);

      const userId = Number(req.params.id);

      if (!userId) {
        throw new ApiError(400, "Identifiant utilisateur invalide.");
      }

      const item = await fetchUserById(client, userId);

      if (!item) {
        throw new ApiError(404, "Utilisateur introuvable.");
      }

      const roles = await fetchRoleOptions(client);
      const companies = await fetchAvailableCompanies(client);
      const settings = await ensureSystemSettings(client);

      await client.query("COMMIT");

      res.json({
        item,
        roles,
        companies,
        password_policy: {
          minimum_length: settings.password_minimum_length,
        },
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

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

    try {
      await client.query("BEGIN");
      await ensureAdminPermission(client, req);

      const settings = await ensureSystemSettings(client);
      const fullName = cleanString(req.body.full_name || req.body.fullName);
      const email = normalizeEmail(req.body.email);
      const role = cleanString(req.body.role);
      const password = String(req.body.password || "");
      const companyIds = parseCompanyIds(req.body.company_ids || req.body.companyIds);
      const allowedModules = parseAllowedModules(req.body.allowed_modules || req.body.allowedModules);
      const defaultModule = normalizeDefaultModule(
        req.body.default_module || req.body.defaultModule,
        allowedModules,
      );
      const isActive = req.body.is_active !== false;

      if (!fullName) {
        throw new ApiError(400, "Le nom complet est obligatoire.");
      }

      if (!email) {
        throw new ApiError(400, "L'adresse e-mail est obligatoire.");
      }

      if (!role) {
        throw new ApiError(400, "Le rôle est obligatoire.");
      }

      if (companyIds.length === 0) {
        throw new ApiError(400, "Sélectionnez au moins une société autorisée.");
      }

      if (password.length < settings.password_minimum_length) {
        throw new ApiError(
          400,
          `Le mot de passe doit contenir au moins ${settings.password_minimum_length} caractères.`,
        );
      }

      await ensureUniqueEmail(client, email);
      await ensureRoleExists(client, role);
      await ensureCompaniesExist(client, companyIds);

      const passwordHash = await bcrypt.hash(password, PASSWORD_SALT_ROUNDS);
      const primaryCompanyId = companyIds[0];

      const { rows } = await client.query(
        `
          INSERT INTO users (
            email,
            password_hash,
            full_name,
            role,
            company_id,
            allowed_modules,
            default_module,
            is_active,
            failed_login_attempts,
            locked_at
          )
          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 0, NULL)
          RETURNING id
        `,
        [
          email,
          passwordHash,
          fullName,
          role,
          primaryCompanyId,
          allowedModules,
          defaultModule,
          isActive,
        ],
      );

      const userId = Number(rows[0].id);

      await syncUserCompanies(client, userId, companyIds);

      const createdUser = await fetchUserById(client, userId);

      await logAudit(client, {
        companyId: primaryCompanyId,
        userId: req.user?.sub || null,
        action: "users.create",
        entityType: "user",
        entityId: userId,
        newData: createdUser,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      req.app.get("io")?.to("admins")?.emit("user:created", {
        id: createdUser.id,
        full_name: createdUser.full_name,
        email: createdUser.email,
        role_label: createdUser.role_label,
      });

      res.status(201).json({
        message: "Utilisateur créé avec succès.",
        item: createdUser,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

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

    try {
      await client.query("BEGIN");
      await ensureAdminPermission(client, req);

      const userId = Number(req.params.id);

      if (!userId) {
        throw new ApiError(400, "Identifiant utilisateur invalide.");
      }

      const { rows: currentRows } = await client.query(
        `
          SELECT *
          FROM users
          WHERE id = $1
          LIMIT 1
        `,
        [userId],
      );

      if (!currentRows[0]) {
        throw new ApiError(404, "Utilisateur introuvable.");
      }

      const currentUser = currentRows[0];
      const settings = await ensureSystemSettings(client);
      const fullName = cleanString(req.body.full_name || req.body.fullName || currentUser.full_name);
      const email = normalizeEmail(req.body.email || currentUser.email);
      const role = cleanString(req.body.role || currentUser.role);
      const password = String(req.body.password || "");
      const companyIds = parseCompanyIds(
        req.body.company_ids ||
          req.body.companyIds ||
          (await fetchUserById(client, userId))?.company_ids ||
          [],
      );
      const allowedModules = parseAllowedModules(
        req.body.allowed_modules || req.body.allowedModules || currentUser.allowed_modules,
      );
      const defaultModule = normalizeDefaultModule(
        req.body.default_module || req.body.defaultModule || currentUser.default_module,
        allowedModules,
      );
      const isActive =
        req.body.is_active === undefined ? currentUser.is_active : Boolean(req.body.is_active);

      if (!fullName) {
        throw new ApiError(400, "Le nom complet est obligatoire.");
      }

      if (!email) {
        throw new ApiError(400, "L'adresse e-mail est obligatoire.");
      }

      if (!role) {
        throw new ApiError(400, "Le rôle est obligatoire.");
      }

      if (companyIds.length === 0) {
        throw new ApiError(400, "Sélectionnez au moins une société autorisée.");
      }

      if (password && password.length < settings.password_minimum_length) {
        throw new ApiError(
          400,
          `Le mot de passe doit contenir au moins ${settings.password_minimum_length} caractères.`,
        );
      }

      await ensureUniqueEmail(client, email, userId);
      await ensureRoleExists(client, role);
      await ensureCompaniesExist(client, companyIds);

      const previousSnapshot = await fetchUserById(client, userId);
      const passwordHash = password
        ? await bcrypt.hash(password, PASSWORD_SALT_ROUNDS)
        : currentUser.password_hash;
      const unlockAccount = Boolean(password) || isActive;
      const primaryCompanyId = companyIds[0];

      await client.query(
        `
          UPDATE users
          SET
            full_name = $2,
            email = $3,
            role = $4,
            company_id = $5,
            allowed_modules = $6,
            default_module = $7,
            is_active = $8,
            password_hash = $9,
            failed_login_attempts = CASE WHEN $10 THEN 0 ELSE failed_login_attempts END,
            locked_at = CASE WHEN $10 THEN NULL ELSE locked_at END
          WHERE id = $1
        `,
        [
          userId,
          fullName,
          email,
          role,
          primaryCompanyId,
          allowedModules,
          defaultModule,
          isActive,
          passwordHash,
          unlockAccount,
        ],
      );

      await syncUserCompanies(client, userId, companyIds);

      const updatedUser = await fetchUserById(client, userId);

      await logAudit(client, {
        companyId: primaryCompanyId,
        userId: req.user?.sub || null,
        action: "users.update",
        entityType: "user",
        entityId: userId,
        oldData: previousSnapshot,
        newData: updatedUser,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      req.app.get("io")?.to("admins")?.emit("user:updated", {
        id: updatedUser.id,
        full_name: updatedUser.full_name,
        email: updatedUser.email,
        role_label: updatedUser.role_label,
        is_active: updatedUser.is_active,
      });

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

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

    try {
      await client.query("BEGIN");
      await ensureAdminPermission(client, req);

      const userId = Number(req.params.id);

      if (!userId) {
        throw new ApiError(400, "Identifiant utilisateur invalide.");
      }

      const previousSnapshot = await fetchUserById(client, userId);

      if (!previousSnapshot) {
        throw new ApiError(404, "Utilisateur introuvable.");
      }

      const allowedModules = parseAllowedModules(req.body.allowed_modules || req.body.allowedModules);
      const defaultModule = normalizeDefaultModule(
        req.body.default_module || req.body.defaultModule,
        allowedModules,
      );

      await client.query(
        `
          UPDATE users
          SET
            allowed_modules = $2,
            default_module = $3
          WHERE id = $1
        `,
        [userId, allowedModules, defaultModule],
      );

      const updatedUser = await fetchUserById(client, userId);

      await logAudit(client, {
        companyId: updatedUser.company_id,
        userId: req.user?.sub || null,
        action: "users.modules.update",
        entityType: "user",
        entityId: userId,
        oldData: {
          allowed_modules: previousSnapshot.allowed_modules,
          default_module: previousSnapshot.default_module,
        },
        newData: {
          allowed_modules: updatedUser.allowed_modules,
          default_module: updatedUser.default_module,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      req.app.get("io")?.to("admins")?.emit("user:updated", {
        id: updatedUser.id,
        full_name: updatedUser.full_name,
        email: updatedUser.email,
        role_label: updatedUser.role_label,
        allowed_modules: updatedUser.allowed_modules,
        default_module: updatedUser.default_module,
      });

      res.json({
        message: "Modules utilisateur mis à jour.",
        item: updatedUser,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

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

    try {
      await client.query("BEGIN");
      await ensureAdminPermission(client, req);

      const userId = Number(req.params.id);

      if (!userId) {
        throw new ApiError(400, "Identifiant utilisateur invalide.");
      }

      if (userId === Number(req.user?.sub)) {
        throw new ApiError(400, "Vous ne pouvez pas désactiver votre propre compte.");
      }

      const previousSnapshot = await fetchUserById(client, userId);

      if (!previousSnapshot) {
        throw new ApiError(404, "Utilisateur introuvable.");
      }

      await client.query(
        `
          UPDATE users
          SET
            is_active = FALSE,
            locked_at = NULL
          WHERE id = $1
        `,
        [userId],
      );

      const updatedUser = await fetchUserById(client, userId);

      await logAudit(client, {
        companyId: updatedUser.company_id,
        userId: req.user?.sub || null,
        action: "users.delete",
        entityType: "user",
        entityId: userId,
        oldData: previousSnapshot,
        newData: updatedUser,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      req.app.get("io")?.to("admins")?.emit("user:updated", {
        id: updatedUser.id,
        full_name: updatedUser.full_name,
        email: updatedUser.email,
        role_label: updatedUser.role_label,
        is_active: updatedUser.is_active,
      });

      res.json({
        message: "Utilisateur désactivé.",
        item: updatedUser,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },
};
