import { CookieOptions, Request, Response } from "express";
import bcrypt from "bcrypt";
import asyncHandler from "../common/asyncHandler";
import Admin from "../models/admin";
import { Status } from "../models/types";
import jwt, { SignOptions } from "jsonwebtoken";
import * as helper from "../common/helper";
import * as env from "../config/env.config";
import * as authHelper from "../common/authHelper";
import Token from "../models/Token";
import * as mailHelper from "../common/mailHelper";
import * as logger from "../common/logger";
import {
  forgotPasswordAdminTemplate,
  registerAdminTemplate,
} from "../common/templates";
import { fileURLToPath } from "url";
import path from "path";
import { Types } from "mongoose";
import PushToken from "../models/PushToken";

/**
 * Autentica a un administrador y retorna un JWT de acceso.
 *
 * Verifica email, estado activo y contraseña (bcrypt). Genera el token JWT
 * incluyendo `id`, `email`, `roles` y `modules` del admin. Si `rememberMe` es
 * `true`, la cookie tiene una vigencia de 400 días; de lo contrario, usa el
 * valor de `JWT_EXPIRE_AT`.
 *
 * @export
 * @async
 * @param {Request} req - Body: `email`, `password`, `rememberMe?`
 * @param {Response} res
 * @returns {unknown}
 */
export const login = asyncHandler(async (req: Request, res: Response) => {
  const { email, password, rememberMe } = req.body;

  if (!email || !password) {
    return res.status(400).json({
      success: false,
      message: "Correo y contraseña son requeridos.",
      code: "field_missing",
    });
  }

  const user = await Admin.findOne({ email }).populate({
    path: "roles", // El campo en tu modelo Admin que contiene los ObjectId de los roles
    select: "code modules", // Asegúrate de seguir seleccionando el campo 'nombre'
    populate: {
      path: "modules", // El campo en tu modelo Role que contiene los ObjectId de los módulos
      select: "code", // O los campos que quieras proyectar del modelo de Módulo
    },
  });

  if (!user) {
    return res.status(401).json({
      success: false,
      message: "El correo ingresado no esta registrado.",
      code: "user_not_found",
    });
  }

  if (user.status !== Status.Active) {
    return res.status(401).json({
      success: false,
      message:
        "El usuario ha sido desactivado, por favor contacte con el administrador.",
      code: "user_not_active",
    });
  }

  if (!(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({
      success: false,
      message: "La contraseña ingresada es incorrecta.",
      code: "invalid_password",
    });
  }

  let options: SignOptions;
  const cookieOptions: CookieOptions = helper.clone(env.COOKIE_OPTIONS);
  if (rememberMe) {
    options = {
      expiresIn: env.JWT_EXPIRE_AT,
      algorithm: "HS256",
      issuer: "Pasta",
    };
    cookieOptions.maxAge = 400 * 24 * 60 * 60 * 1000;
  } else {
    options = {
      expiresIn: env.JWT_EXPIRE_AT,
      algorithm: "HS256",
      issuer: "Pasta",
    };
    cookieOptions.maxAge = env.JWT_EXPIRE_AT * 1000;
  }

  const token = jwt.sign(
    {
      id: user._id,
      email: user.email,
      roles: user.roles.map((role) => role.code),
      modules: user.roles.flatMap((role) =>
        role.modules.map((module) => module.code),
      ),
    },
    env.JWT_SECRET,
    options,
  );

  return res.status(200).json({
    success: true,
    message: "Login successful",
    data: {
      id: user._id,
      name: user.name,
      lastname: user.lastname,
      email: user.email,
    },
    token,
  });
});

/**
 * Retorna los datos del administrador autenticado actualmente.
 *
 * Usa el `id` del JWT para buscar el documento en la colección `admins`.
 *
 * @export
 * @async
 * @param {Request} req
 * @param {Response} res
 * @returns {unknown}
 */
export const getCurrentUser = asyncHandler(
  async (req: Request, res: Response) => {
    const user = await Admin.findById(req.user!.id);
    if (!user) {
      return res.status(404).json({
        success: false,
        message: "User not found",
        code: "user_not_found",
      });
    }
    return res.status(200).json({
      success: true,
      message: "User retrieved successfully",
      data: user,
    });
  },
);

/**
 * Cierra la sesión del administrador eliminando la cookie de acceso.
 *
 * @export
 * @async
 * @param {Request} req
 * @param {Response} res
 * @returns {unknown}
 */
export const logout = asyncHandler(async (req: Request, res: Response) => {
  res.clearCookie(env.X_ACCESS_TOKEN);
  return res.status(200).json({ success: true, message: "Logout successful" });
});

/**
 * Crea un nuevo administrador y le envía un correo para establecer su contraseña.
 *
 * Flujo:
 * 1. Valida que `name`, `lastname` y `email` estén presentes.
 * 2. Verifica que el email no esté ya registrado.
 * 3. Crea el documento `Admin` con estado `active`.
 * 4. Genera un token de tipo `password-admin-reset` y lo guarda en `Token`.
 * 5. Envía el correo de bienvenida con el link de confirmación.
 * 6. Si el envío de correo falla, elimina el admin recién creado (rollback).
 *
 * @export
 * @async
 * @param {Request} req - Body: `name`, `lastname`, `email`, `roles?`
 * @param {Response} res
 * @returns {unknown}
 */
export const createAdmin = asyncHandler(async (req: Request, res: Response) => {
  const { body } = req;
  if (!body.name || !body.lastname || !body.email) {
    return res.status(400).send("name, lastname, email are required");
  }

  body.email = helper.trim(body.email, " ").toLowerCase();

  const user = await Admin.findOne({ email: body.email });
  if (user) {
    return res.status(400).send("Correo ya esta registrado.");
  }

  const admin = new Admin({
    name: body.name,
    lastname: body.lastname,
    email: body.email,
    roles: body.roles ?? [],
    status: "active",
  });
  await admin.save();

  //
  // Send confirmation email
  //
  try {
    // generate token and save
    const token = new Token({
      user: admin._id,
      token: helper.generateToken(),
      expireAt: Date.now() + env.TOKEN_EXPIRE_AT * 1000,
      type: "password-admin-reset",
    });

    await token.save();

    // Send email
    const mailOptions = {
      from: env.SMTP_FROM,
      to: body.email,
      subject: "Completa tu registro en Pasta Admin",
      html: registerAdminTemplate(
        `${env.ADMIN_URL}/users/confirm-email/${admin._id}/${token.token}`,
      ),
    };

    await mailHelper.sendMail(mailOptions);
    return res
      .status(201)
      .json({ success: true, message: "Admin created successfully" });
  } catch (err) {
    try {
      //
      // Delete user in case of smtp failure
      //
      await admin.deleteOne();
    } catch (deleteErr) {
      logger.error(
        `Error deleting user ${admin._id} after SMTP failure`,
        deleteErr,
      );
    }
    logger.error(`Error sending SMTP for user ${admin.email}`, err);
    return res
      .status(400)
      .send("Hubo un error enviando el correo de confirmación");
  }
});

/**
 * Inicia el flujo de recuperación de contraseña para un administrador.
 *
 * Genera un token de tipo `password-admin-reset`, lo guarda con su fecha de
 * expiración y envía el correo con el link para restablecer la contraseña.
 * Si el email no está registrado, responde con 400.
 *
 * @export
 * @async
 * @param {Request} req - Body: `email`
 * @param {Response} res
 * @returns {unknown}
 */
export const forgotPassword = asyncHandler(
  async (req: Request, res: Response) => {
    const { body } = req;
    if (!body.email) {
      return res.status(400).send("email is required");
    }

    body.email = helper.trim(body.email, " ").toLowerCase();

    const admin = await Admin.findOne({ email: body.email });
    if (!admin) {
      return res.status(400).send("Correo no registrado");
    }

    //
    // Send confirmation email
    //
    try {
      // generate token and save
      const token = new Token({
        user: admin._id,
        token: helper.generateToken(),
        expireAt: new Date(Date.now() + env.TOKEN_EXPIRE_AT * 1000),
        type: "password-admin-reset",
      });

      await token.save();

      // Send email
      const mailOptions = {
        from: env.SMTP_FROM,
        to: body.email,
        subject: "Recupera tu contraseña en Pasta Admin",
        html: forgotPasswordAdminTemplate(
          `${env.ADMIN_URL}/users/confirm-email/${admin._id}/${token.token}`,
        ),
      };

      mailHelper.sendMail(mailOptions);
      return res
        .status(201)
        .json({ success: true, message: "Email sent successfully" });
    } catch (err) {
      logger.error(`Error sending SMTP for user ${admin.email}`, err);
      return res
        .status(400)
        .send("Hubo un error enviando el correo de confirmación");
    }
  },
);

/**
 * Confirma el registro o recuperación de contraseña de un administrador.
 *
 * Valida que el token `password-admin-reset` exista en la base de datos y
 * no haya expirado. Si es válido, hashea la nueva contraseña con bcrypt,
 * la guarda en el usuario y elimina todos los tokens de reseteo pendientes.
 *
 * @export
 * @async
 * @param {Request} req - Body: `userId`, `token`, `password`
 * @param {Response} res
 * @returns {unknown}
 */
export const confirmEmail = asyncHandler(
  async (req: Request, res: Response) => {
    const { userId, token, password } = req.body;
    const user = await Admin.findById(userId);
    if (!user) {
      return res.status(404).json({
        success: false,
        message: "User not found",
        code: "user_not_found",
      });
    }

    const tokendb = await Token.findOne({
      user: userId,
      type: "password-admin-reset",
      token,
    });
    if (!tokendb) {
      return res.status(404).json({
        success: false,
        message:
          "Token es incorrecto o ha expirado. Por favor, contacte con el administrador para volver a enviar el correo de confirmación.",
        code: "token_not_found",
      });
    }

    const salt = await bcrypt.genSalt(10);
    const passwordHash = await bcrypt.hash(password, salt);
    user.password = passwordHash;
    await user.save();
    await Token.deleteMany({ user: userId, type: "password-admin-reset" });

    return res.status(200).json({
      success: true,
      message: "Password updated successfully",
      data: { userId: user._id },
    });
  },
);

/**
 * Verifica que la sesión del administrador sigue activa.
 *
 * Este endpoint se protege con el middleware de autenticación JWT. Si el
 * request llega aquí, el token ya fue validado, por lo que solo responde 200.
 * Se usa para heartbeat o verificación previa al uso de rutas protegidas.
 *
 * @export
 * @async
 * @param {Request} req
 * @param {Response} res
 * @returns {unknown}
 */
export const verifySession = async (req: Request, res: Response) => {
  return res.status(200).json({ success: true, message: "Session verified" });
};

/**
 * Renueva el JWT del administrador autenticado.
 *
 * Genera un nuevo token con los datos del usuario (`id`, `email`, `roles`) y
 * lo establece en la cookie correspondiente. Extiende así la sesión activa
 * sin necesidad de volver a ingresar credenciales.
 *
 * @export
 * @async
 * @param {Request} req
 * @param {Response} res
 * @returns {unknown}
 */
export const refreshToken = async (req: Request, res: Response) => {
  const { user } = req;
  const cookieName = authHelper.getAuthCookieName(req);
  const options = {
    expiresIn: env.JWT_EXPIRE_AT,
    algorithm: "HS256",
    issuer: "Pasta",
  } as SignOptions;
  const token = jwt.sign(
    { id: user?.id, email: user?.email, roles: user?.roles },
    env.JWT_SECRET,
    options,
  );
  res.cookie(cookieName, token, env.COOKIE_OPTIONS);
  return res
    .status(200)
    .json({ success: true, message: "Token refreshed successfully" });
};

/**
 * Retorna la lista de todos los administradores registrados en el sistema.
 * Incluye los roles de cada admin via populate (`_id` y `name`).
 *
 * @export
 * @async
 * @param {Request} req
 * @param {Response} res
 * @returns {unknown}
 */
export const getUsers = asyncHandler(async (req: Request, res: Response) => {
  const results = await Admin.find({}).populate({
    path: "roles",
    select: "_id name",
  });

  return res.status(200).json({
    success: true,
    message: "Users retrieved successfully",
    data: results,
  });
});

/**
 * Retorna los datos del administrador autenticado (perfil propio).
 * Usa el `id` del JWT; no recibe parámetros externos.
 *
 * @export
 * @async
 * @param {Request} req
 * @param {Response} res
 * @returns {unknown}
 */
export const getUser = asyncHandler(async (req: Request, res: Response) => {
  const user = await Admin.findById(req.user!.id);
  if (!user) {
    return res.status(404).json({
      success: false,
      message: "User not found",
      code: "user_not_found",
    });
  }
  return res.status(200).json({
    success: true,
    message: "User retrieved successfully",
    data: user,
  });
});

/**
 * Retorna un administrador específico por su ID (uso administrativo).
 * Incluye los roles del admin via populate completo.
 *
 * @export
 * @async
 * @param {Request} req - Params: `id` (ID del admin)
 * @param {Response} res
 * @returns {unknown}
 */
export const getUserById = asyncHandler(async (req: Request, res: Response) => {
  const { id } = req.params;
  const results = await Admin.findById(id).populate("roles");
  if (!results) {
    return res.status(404).json({
      success: false,
      message: "User not found",
      code: "user_not_found",
    });
  }
  return res.status(200).json({
    success: true,
    message: "User retrieved successfully",
    data: results,
  });
});

/**
 * Actualiza los datos de un administrador específico.
 *
 * Solo permite modificar los campos `name`, `lastname`, `email`, `roles` y `status`
 * (filtrado con `filterObj`). Maneja el error de duplicado de email (código 11000)
 * retornando un mensaje descriptivo en lugar de un error genérico.
 *
 * @export
 * @async
 * @param {Request} req - Params: `id` (ID del admin); Body: campos editables
 * @param {Response} res
 * @returns {unknown}
 */
export const editUser = asyncHandler(async (req: Request, res: Response) => {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);
  const { id } = req.params;
  console.log(__dirname);
  const filteredBody = helper.filterObj(
    req.body,
    "name",
    "lastname",
    "email",
    "roles",
    "status",
  );
  try {
    const user = await Admin.findById(id);
    if (!user) {
      return res.status(404).json({ message: "User not found" });
    }
    Object.assign(user, filteredBody);
    await user!.save();
  } catch (error: any) {
    if (error.code === 11000) {
      let message = "Error: Ya existe un registro con ";

      if (error.keyPattern && error.keyValue) {
        if (error.keyPattern.email) {
          message += `este correo electrónico (${error.keyValue.email}).`;
        } else {
          message = "Error: Ya existe un registro con valores duplicados.";
        }
      } else {
        message = "Error: Ya existe un registro con valores duplicados.";
      }

      return res.status(400).json({
        success: false,
        code: "user_exists",
        message,
      });
    }
    return res.status(400).json({ success: false, message: error.message });
  }

  return res
    .status(200)
    .json({ success: true, message: "User updated successfully" });
});

/**
 * Registra un token FCM para que el administrador autenticado reciba notificaciones push.
 *
 * Realiza un upsert en `PushToken` con `ownerType: "ADMIN"` y mantiene el token
 * también en el array `pushTokens` del admin por compatibilidad con versiones anteriores.
 * Retorna el total de tokens activos del admin.
 *
 * @export
 * @async
 * @param {Request} req - Body: `token` (FCM device token)
 * @param {Response} res
 * @returns {unknown}
 */
export const subscribePush = asyncHandler(
  async (req: Request, res: Response) => {
    const adminId = req.user!.id as unknown as Types.ObjectId;
    const { token } = req.body as { token: string };
    const admin = await Admin.findById(adminId);
    if (!admin) {
      return res.status(404).json({
        success: false,
        message: "User not found",
        code: "user_not_found",
      });
    }
    // Upsert in PushToken collection
    await PushToken.findOneAndUpdate(
      { token },
      {
        $set: {
          token,
          ownerType: "ADMIN",
          adminId: admin._id,
          userId: null,
          status: "active",
          lastUsedAt: new Date(),
        },
      },
      { upsert: true, new: true },
    );

    // Keep last token for compatibility
    (admin as any).pushToken = token;
    const current: string[] = Array.isArray((admin as any).pushTokens)
      ? ((admin as any).pushTokens as string[])
      : [];
    if (!current.includes(token)) current.push(token);
    (admin as any).pushTokens = current;
    await admin.save();

    const activeCount = await PushToken.countDocuments({
      ownerType: "ADMIN",
      adminId: admin._id,
      status: "active",
    });
    return res.json({
      success: true,
      message: "Push token subscribed successfully",
      data: { tokens: activeCount },
    });
  },
);

/**
 * Desactiva un token FCM del administrador para que el dispositivo deje de recibir notificaciones push.
 *
 * Marca el token como `inactive` en `PushToken` y lo elimina del array `pushTokens` del admin.
 * Si era el token principal, lo reemplaza con el siguiente token activo disponible.
 * Retorna el total de tokens activos restantes del admin.
 *
 * @export
 * @async
 * @param {Request} req - Body: `token` (FCM device token a desactivar)
 * @param {Response} res
 * @returns {unknown}
 */
export const unsubscribePush = asyncHandler(
  async (req: Request, res: Response) => {
    const adminId = req.user!.id as unknown as Types.ObjectId;
    const { token } = req.body as { token: string };
    const admin = await Admin.findById(adminId);
    if (!admin) {
      return res.status(404).json({
        success: false,
        message: "User not found",
        code: "user_not_found",
      });
    }
    // Mark token as inactive in PushToken collection
    await PushToken.updateOne(
      { token },
      { $set: { status: "inactive", lastUsedAt: new Date() } },
    );

    const current: string[] = Array.isArray((admin as any).pushTokens)
      ? ((admin as any).pushTokens as string[])
      : [];
    const next = current.filter((t) => t !== token);
    (admin as any).pushTokens = next;
    if ((admin as any).pushToken === token) {
      (admin as any).pushToken = next[0] || undefined;
    }
    await admin.save();

    const activeCount = await PushToken.countDocuments({
      ownerType: "ADMIN",
      adminId: admin._id,
      status: "active",
    });
    return res.json({
      success: true,
      message: "Push token unsubscribed successfully",
      data: { tokens: activeCount },
    });
  },
);

/**
 * Genera un JWT con módulos/roles específicos sin necesidad de autenticación previa.
 *
 * Útil para crear tokens de acceso temporal o de integración con permisos acotados.
 * El token no tiene fecha de expiración explícita (solo la configuración global del servidor).
 *
 * @export
 * @async
 * @param {Request} req - Body: `roles` (array de códigos de módulo a incluir en el token)
 * @param {Response} res
 * @returns {unknown}
 */
export const generateToken = asyncHandler(
  async (req: Request, res: Response) => {
    const { roles } = req.body;
    const token = jwt.sign({ modules: roles }, env.JWT_SECRET, {
      issuer: "Pasta",
    });
    return res.status(200).json({
      success: true,
      message: "Token generated successfully",
      data: { token },
    });
  },
);
