import { Router } from "express";
import { z } from "zod";
import type { Request } from "express";
import { config } from "../../config.js";
import { one, query, transaction } from "../../db/client.js";
import { hashPassword } from "../../lib/crypto.js";
import { audit } from "../../lib/audit.js";
import { badRequest, notFound } from "../../lib/errors.js";
import { requireAuth } from "../../middleware/auth.js";
import { requirePermission } from "../../middleware/rbac.js";
import { validateBody } from "../../middleware/validate.js";

export const adminUsersRouter = Router();

/** Link (or unlink) a member file to an account — admin action, audited. */
adminUsersRouter.put(
  "/:id/member-link",
  requirePermission("admin.manage_users"),
  validateBody(
    z.object({
      memberUuid: z.string().uuid("معرف العضو غير صالح").nullable(),
      reason: z.string().min(5).max(500),
    }),
  ),
  async (req, res, next) => {
    try {
      const userId = z.coerce.number().int().positive().parse(req.params.id);
      let memberId: number | null = null;
      if (req.body.memberUuid) {
        const m = await one(`SELECT id FROM members WHERE public_uuid = $1::uuid AND deleted_at IS NULL`, [req.body.memberUuid]);
        if (!m) throw badRequest("العضو غير موجود", "member_not_found");
        const taken = await one(
          `SELECT id, username FROM users WHERE member_id = (SELECT id FROM members WHERE public_uuid = $2::uuid) AND id <> $1`,
          [userId, req.body.memberUuid],
        );
        if (taken) throw badRequest("هذا الملف مرتبط بحساب آخر (" + taken.username + ") — شخص واحد = حساب واحد", "member_link_taken");
        memberId = m.id;
      }
      const prev = await one<{ member_id: number | null; username: string }>(`SELECT member_id, username FROM users WHERE id = $1`, [userId]);
      if (!prev) throw notFound("الحساب غير موجود");
      await query(`UPDATE users SET member_id = $2 WHERE id = $1`, [userId, memberId]);
      await audit({
        actorUserId: req.user!.id,
        action: "users.member_linked",
        entityType: "user",
        entityId: userId,
        changedFields: { from: prev.member_id, to: memberId },
        context: { reason: req.body.reason, username: prev.username },
        ip: req.ip ?? null,
        userAgent: req.headers["user-agent"] ?? null,
      });
      res.json({ ok: true, memberId });
    } catch (error) {
      next(error);
    }
  },
);
adminUsersRouter.use(requireAuth, requirePermission("admin.manage_users"));

const listQuery = `
  SELECT u.id, u.username, u.full_name, u.email, u.is_active, u.must_change_password, u.is_demo,
         u.locked_until::text, u.last_login_at::text, u.created_at::text,
         COALESCE((
           SELECT json_agg(json_build_object('code', r.code, 'nameAr', r.name_ar))
             FROM user_roles ur JOIN roles r ON r.id = ur.role_id
            WHERE ur.user_id = u.id AND ur.revoked_at IS NULL
               AND ur.valid_from <= now() AND (ur.valid_until IS NULL OR ur.valid_until > now())
         ), '[]'::json) AS roles
  FROM users u
`;

adminUsersRouter.get("/", async (req: Request, res, next) => {
  try {
    const page = Math.max(1, Number.parseInt(String(req.query.page ?? "1"), 10) || 1);
    const pageSize = Math.min(50, Math.max(5, Number.parseInt(String(req.query.pageSize ?? "20"), 10) || 20));
    const offset = (page - 1) * pageSize;
    const search = typeof req.query.q === "string" ? `%${req.query.q.trim()}%` : null;

    const where = search ? `WHERE u.username ILIKE $1 OR u.full_name ILIKE $1` : "";
    const params = search ? [search, pageSize, offset] : [pageSize, offset];
    // param positions differ by branch; build explicitly to keep $n alignment
    const rows = await query(
      search
        ? `${listQuery} ${where} ORDER BY u.id LIMIT $2 OFFSET $3`
        : `${listQuery} ORDER BY u.id LIMIT $1 OFFSET $2`,
      params,
    );
    const total = await one<{ count: string }>(
      search ? `SELECT COUNT(*)::text AS count FROM users u WHERE u.username ILIKE $1 OR u.full_name ILIKE $1` : `SELECT COUNT(*)::text AS count FROM users`,
      search ? [search] : [],
    );
    res.json({ items: rows.rows, page, pageSize, total: Number(total?.count ?? 0) });
  } catch (error) {
    next(error);
  }
});

adminUsersRouter.post(
  "/",
  validateBody(
    z.object({
      username: z
        .string()
        .min(3)
        .max(64)
        .regex(/^[a-zA-Z0-9_.-]+$/, "اسم المستخدم يقبل حروفًا لاتينية وأرقامًا و . _ - فقط"),
      fullName: z.string().min(3).max(255),
      email: z.string().email().max(255).optional().nullable(),
      tempPassword: z.string().min(10).max(128),
    }),
  ),
  async (req, res, next) => {
    try {
      const { username, fullName, email, tempPassword } = req.body;
      const exists = await one(`SELECT id FROM users WHERE lower(username) = lower($1)`, [username]);
      if (exists) throw badRequest("اسم المستخدم مستخدم بالفعل", "username_taken");

      const hash = await hashPassword(tempPassword);
      const row = await one<{ id: number }>(
        `INSERT INTO users (username, full_name, email, password_hash, is_active, must_change_password, created_by)
         VALUES ($1, $2, $3, $4, TRUE, TRUE, $5) RETURNING id`,
        [username.trim(), fullName.trim(), email ?? null, hash, req.user!.id],
      );
      await audit({
        actorUserId: req.user!.id,
        action: "admin.user.created",
        entityType: "user",
        entityId: row!.id,
        changedFields: { username, fullName, email: email ?? null },
        ip: req.ip ?? null,
        userAgent: req.headers["user-agent"] ?? null,
      });
      res.status(201).json({ id: row!.id, username, mustChangePassword: true });
    } catch (error) {
      next(error);
    }
  },
);

adminUsersRouter.patch(
  "/:id/activation",
  validateBody(
    z.object({
      activate: z.boolean(),
      reason: z.string().min(3).max(500),
    }),
  ),
  async (req, res, next) => {
    try {
      const id = Number.parseInt(req.params.id as string, 10);
      const target = await one<{ id: number; username: string; is_active: boolean }>(`SELECT id, username, is_active FROM users WHERE id = $1`, [id]);
      if (!target) throw notFound("المستخدم غير موجود");
      if (target.id === req.user!.id && !req.body.activate) {
        throw badRequest("لا يمكنك تعطيل حسابك الخاص", "self_deactivation");
      }
      await query(`UPDATE users SET is_active = $2, updated_at = now(), updated_by = $3 WHERE id = $1`, [
        id,
        req.body.activate,
        req.user!.id,
      ]);
      await audit({
        actorUserId: req.user!.id,
        action: req.body.activate ? "admin.user.activated" : "admin.user.deactivated",
        entityType: "user",
        entityId: id,
        changedFields: { reason: req.body.reason },
        ip: req.ip ?? null,
        userAgent: req.headers["user-agent"] ?? null,
      });
      res.json({ ok: true });
    } catch (error) {
      next(error);
    }
  },
);

adminUsersRouter.post(
  "/:id/unlock",
  async (req, res, next) => {
    try {
      const id = Number.parseInt(req.params.id as string, 10);
      const target = await one<{ id: number }>(`SELECT id FROM users WHERE id = $1`, [id]);
      if (!target) throw notFound("المستخدم غير موجود");
      await query(`UPDATE users SET failed_login_count = 0, locked_until = NULL, updated_at = now(), updated_by = $2 WHERE id = $1`, [id, req.user!.id]);
      await audit({
        actorUserId: req.user!.id,
        action: "admin.user.unlocked",
        entityType: "user",
        entityId: id,
        ip: req.ip ?? null,
        userAgent: req.headers["user-agent"] ?? null,
      });
      res.json({ ok: true });
    } catch (error) {
      next(error);
    }
  },
);

/** Assign a role to a user. Enforces governance confirmation + mandatory documented reason. */
adminUsersRouter.post(
  "/:id/roles",
  validateBody(
    z.object({
      validUntil: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional(),
      roleCode: z.string().min(2).max(64),
      reason: z.string().min(5).max(500),
      governorateIds: z.array(z.number().int().positive()).max(27).optional(),
      allowUnconfirmedRole: z.boolean().optional(),
    }),
  ),
  async (req, res, next) => {
    try {
      const userId = Number.parseInt(req.params.id as string, 10);
      const { roleCode, reason, governorateIds, allowUnconfirmedRole, validUntil } = req.body;

      // Separation of duties: no one may assign a role to themselves (SEC audit finding)
      if (userId === req.user!.id) {
        throw badRequest("لا يجوز إسناد دور لحسابك الشخصي — الإسناد يصدر عن جهة أخرى", "self_assignment_forbidden");
      }

      const target = await one<{ id: number }>(`SELECT id FROM users WHERE id = $1`, [userId]);
      if (!target) throw notFound("المستخدم غير موجود");

      const role = await one<{ id: number; requires_scope: boolean; governance_confirmed: boolean; is_enabled: boolean }>(
        `SELECT id, requires_scope, governance_confirmed, is_enabled FROM roles WHERE code = $1`,
        [roleCode],
      );
      if (!role) throw notFound("الدور غير موجود");
      if (!role.is_enabled) throw badRequest("هذا الدور غير مفعّل", "role_disabled");
      // Privilege-escalation guard (decision 10): the technical system_admin role is
      // never assignable through the API — technical admins hold no organizational authority.
      if (roleCode === "system_admin") {
        throw badRequest(
          "دور مسؤول النظام تقني ولا يُسنَد عبر الواجهة — القرار التنظيمي خارج نطاق النظام",
          "system_admin_not_assignable",
        );
      }
      const cairoToday = new Intl.DateTimeFormat("en-CA", { timeZone: "Africa/Cairo" }).format(new Date());
      if (validUntil && validUntil < cairoToday) {
        throw badRequest("تاريخ انتهاء الصلاحية في الماضي", "valid_until_in_past");
      }
      // Governance rule (AUTHORIZATION_MATRIX §7): unconfirmed roles must never be assigned.
      // The explicit bypass is a DEVELOPMENT/TEST-ONLY tool and is rejected in production (SEC-1 fix).
      if (!role.governance_confirmed && !allowUnconfirmedRole) {
        throw badRequest(
          "هذا الدور لم تؤكده الحوكمة بعد (G-1/Q3) — لا يجوز إسناده. يمكن تجاوز ذلك في بيئة التطوير فقط بخيار صريح.",
          "role_not_governance_confirmed",
        );
      }
      if (!role.governance_confirmed && allowUnconfirmedRole) {
        if (config.isProduction) {
          throw badRequest("تجاوز تأكيد الحوكمة غير متاح في بيئة الإنتاج", "bypass_forbidden_in_production");
        }
        console.warn(
          `[roles] DEV BYPASS: assigning unconfirmed role '${roleCode}' to user ${userId} — reason: ${reason}`,
        );
      }
      if (role.requires_scope && (!governorateIds || governorateIds.length === 0)) {
        throw badRequest("هذا الدور محصور بنطاق — يجب تحديد محافظة واحدة على الأقل", "scope_required");
      }
      if (!role.requires_scope && governorateIds && governorateIds.length > 0) {
        throw badRequest("هذا الدور وطني — لا يقبل نطاق محافظة", "scope_not_allowed");
      }
      if (governorateIds?.length) {
        const valid = await one<{ count: string }>(
          `SELECT COUNT(*)::text AS count FROM governorates WHERE id = ANY($1::int[])`,
          [governorateIds],
        );
        if (Number(valid?.count ?? 0) !== governorateIds.length) {
          throw badRequest("إحدى المحافظات المحددة غير موجودة", "invalid_scope");
        }
      }

      const result = await transaction(async (client) => {
        const existing = await client.query(
          `SELECT id FROM user_roles WHERE user_id = $1 AND role_id = $2 AND revoked_at IS NULL FOR UPDATE`,
          [userId, role.id],
        );
        if (existing.rows.length > 0) throw badRequest("الدور مسند بالفعل لهذا المستخدم", "already_assigned");

        const inserted = await client.query<{ id: number }>(
          `INSERT INTO user_roles (user_id, role_id, granted_by, reason, valid_until)
           VALUES ($1, $2, $3, $4, $5) RETURNING id`,
          [userId, role.id, req.user!.id, reason, validUntil ?? null],
        );
        const userRoleId = inserted.rows[0]!.id;
        if (role.requires_scope && governorateIds?.length) {
          for (const gid of governorateIds) {
            await client.query(
              `INSERT INTO user_role_scopes (user_role_id, scope_type, scope_id) VALUES ($1, 'governorate', $2)
               ON CONFLICT DO NOTHING`,
              [userRoleId, gid],
            );
          }
        }
        await audit(
          {
            actorUserId: req.user!.id,
            action: "roles.assign",
            entityType: "user",
            entityId: userId,
            changedFields: { role: roleCode, governorateIds: governorateIds ?? [], governanceBypass: allowUnconfirmedRole === true, validUntil: validUntil ?? null },
            context: { reason },
            ip: req.ip ?? null,
            userAgent: req.headers["user-agent"] ?? null,
          },
          client,
        );
        return userRoleId;
      });
      res.status(201).json({ ok: true, userRoleId: result });
    } catch (error) {
      next(error);
    }
  },
);

adminUsersRouter.delete(
  "/:id/roles/:roleCode",
  validateBody(
    z.object({
      reason: z.string().min(5).max(500),
    }),
  ),
  async (req, res, next) => {
    try {
      const userId = Number.parseInt(req.params.id as string, 10);
      const roleCode = req.params.roleCode as string;
      const row = await one<{ ur_id: number }>(
        `SELECT ur.id AS ur_id FROM user_roles ur JOIN roles r ON r.id = ur.role_id
          WHERE ur.user_id = $1 AND r.code = $2 AND ur.revoked_at IS NULL`,
        [userId, roleCode],
      );
      if (!row) throw notFound("الدور غير مسند لهذا المستخدم");
      await transaction(async (client) => {
        await client.query(`UPDATE user_roles SET revoked_at = now() WHERE id = $1`, [row!.ur_id]);
        await audit(
          {
            actorUserId: req.user!.id,
            action: "roles.revoke",
            entityType: "user",
            entityId: userId,
            changedFields: { role: roleCode },
            context: { reason: req.body.reason },
            ip: req.ip ?? null,
            userAgent: req.headers["user-agent"] ?? null,
          },
          client,
        );
      });
      res.json({ ok: true });
    } catch (error) {
      next(error);
    }
  },
);
