import { encrypt } from "@/lib/cipher";
import { logError } from "@/lib/logger";
import { createConsoleUserSession } from "@/lib/session";
import { UpdotAuthManager } from "@/lib/updot-auth";
import { TConsoleUserRolesServices } from "../console-user-roles/console-user-roles.services";
import { TConsoleUserSessionServices } from "../console-user-sessions/console-user-session.services";
import { TConsoleUserRepository } from "./console-users.repository";
import { HASH_ALGO, hashPassword, verifyPassword } from "./console-users.utils";

import { env } from "@/lib/env";
import { SMTP_FROM, transporter } from "@/lib/mailer";
import { sendAccountVerificationMail } from "@/lib/templates/account-verification";
import { sendResetPasswordMail } from "@/lib/templates/reset-password";
import {
  CONSOLE_SSO_TOKEN_KIND,
  TVerificationTokenServices,
} from "../verification-tokens/verification-tokens.services";

type TConsoleUserServiceDeps = {
  ConsoleUserRepository: TConsoleUserRepository;
  ConsoleUserSessionServices: TConsoleUserSessionServices;
  ConsoleUserRolesServices: TConsoleUserRolesServices;
  VerificationTokenServices: TVerificationTokenServices;
};

export const ConsoleUserServices = ({
  ConsoleUserRepository,
  ConsoleUserSessionServices,
  ConsoleUserRolesServices,
  VerificationTokenServices,
}: TConsoleUserServiceDeps) => {
  const RegisterNewUser = async (userData: {
    firstName: string;
    lastName: string;
    email: string;
    username: string;
    password?: string;
    isSuperAdmin?: boolean;
    roles?: {
      roleId: string;
    }[];
    isActive?: boolean;
  }) => {
    try {
      const existingWithEmail =
        await ConsoleUserRepository.FindEntryByEmailOrUsername(userData.email);
      if (existingWithEmail) {
        throw new Error("Email already in use");
      }

      const existingWithUsername =
        await ConsoleUserRepository.FindEntryByEmailOrUsername(
          userData.username,
        );

      if (existingWithUsername) {
        throw new Error("Username already in use");
      }

      let hashedPasswordInfo: any = {};
      if (userData.password) {
        const hashedPassword = await hashPassword(userData.password);
        hashedPasswordInfo = {
          hashedPassword: hashedPassword.hash,
          passwordHashAlgo: HASH_ALGO,
          passwordIterations: hashedPassword.iterations,
          passwordSalt: hashedPassword.salt,
        };
      }

      const newUser = await ConsoleUserRepository.CreateEntry({
        email: userData.email,
        firstName: userData.firstName,
        lastName: userData.lastName,
        username: userData.username,
        ...hashedPasswordInfo,
        isSuperAdmin: userData.isSuperAdmin,
        isActive: userData.isActive ?? true,
      });

      // Send invitation email if no password was provided
      if (newUser && !userData.password) {
        try {
          const { token } =
            await VerificationTokenServices.CreateInvitationToken(newUser.id);
          const verificationLink = `${env.GetString("ADMIN_CONSOLE_URL")}/set-password?token=${token}`;

          const mailOptions = await sendAccountVerificationMail(
            {
              name: `${userData.firstName} ${userData.lastName}`,
              verification_link: verificationLink,
              button_name: "Set Password",
            },
            { from: SMTP_FROM, to: userData.email },
          );

          await transporter.sendMail(mailOptions);
        } catch (mailErr) {
          logError("Failed to send invitation email:");
          logError(mailErr);
          // We don't fail registration if only email fails, but maybe we should?
          // For now, just log it.
        }
      }

      // Assign roles if provided
      if (newUser && userData.roles && userData.roles.length > 0) {
        const rolesEntry = userData.roles.map((role) => ({
          userId: newUser.id,
          roleId: role.roleId,
        }));
        await ConsoleUserRolesServices.CreateUserRole(rolesEntry);
      }
      return newUser;
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  const SigninConsoleUser = async (entry: {
    email?: string;
    username?: string;
    password: string;
    browser: string;
    deviceType: string;
    fcmToken: string;
    ipAddress: string;
    location: string;
    os: string;
  }) => {
    try {
      if (!entry?.email && !entry?.username) {
        throw new Error("Provide either username or email");
      }
      const user = await ConsoleUserRepository.FindEntryByEmailOrUsername(
        (entry?.email ?? entry?.username) as string,
      );
      if (!user) {
        throw new Error("Invalid Username or Email");
      }
      if (!user.is_active) {
        throw new Error(
          "Your account is currently inactive. Please contact your administrator.",
        );
      }
      if (
        !user.hashed_password ||
        !user.password_salt ||
        !user.password_iterations
      ) {
        throw new Error(
          "Account not activated. Please check your email and set a password.",
        );
      }

      const passwordVerified = await verifyPassword(
        entry.password,
        user.hashed_password,
        user.password_salt,
        user.password_iterations,
      );

      if (!passwordVerified) {
        throw new Error("Wrong password");
      }

      try {
        if (user.email) {
          const updotSession = UpdotAuthManager.getInstance();
          await updotSession.getOrLogin();
          if (updotSession) {
            console.log(
              `[SigninSync] Generated fresh Updot tokens for ${
                user.email
              }. ID: ${updotSession
                .getSession()
                ?.admin_session_id.substring(0, 8)}...`,
            );
          }
        }
      } catch (e) {
        console.error("[SigninSync] Failed to sync with Updot Core:", e);
      }

      return await StartSession(user, entry);
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  /**
   * Mints the session for an already-authenticated console user.
   *
   * Extracted so password sign-in and one-time-link sign-in produce exactly the
   * same session — the difference between them is only how the user was proven,
   * and that belongs above this call, not inside it.
   */
  const StartSession = async (
    user: { id: string; email: string },
    device: {
      browser?: string;
      deviceType?: string;
      fcmToken?: string;
      ipAddress?: string;
      location?: string;
      os?: string;
    },
  ) => {
    const { sessionToken, dbSessionToken } = await createConsoleUserSession(
      user?.email,
    );
    const encryptedSessionToken = encrypt(sessionToken);
    const newSession = await ConsoleUserSessionServices.CreateSession({
      browser: device.browser ?? "",
      console_user_id: user.id,
      device_type: device.deviceType ?? "",
      fcm_token: device.fcmToken ?? "",
      ip_address: device.ipAddress ?? "",
      location: device.location ?? "",
      os: device.os ?? "",
      refresh_token: dbSessionToken,
    });

    return {
      ...newSession,
      encryptedSessionToken,
    };
  };

  /**
   * Signs a user in from a one-time link sent to their email address.
   *
   * The token is redeemed before anything else and is gone either way, so a
   * forwarded or re-clicked link cannot produce a second session. Delivery to the
   * address on the account is what stands in for the password here, which is the
   * same assumption the invitation and password-reset links already make.
   */
  const SsoSigninConsoleUser = async (entry: {
    token: string;
    browser?: string;
    deviceType?: string;
    fcmToken?: string;
    ipAddress?: string;
    location?: string;
    os?: string;
  }) => {
    try {
      const tokenRecord = await VerificationTokenServices.ConsumeToken(
        entry.token,
        CONSOLE_SSO_TOKEN_KIND,
      );
      if (!tokenRecord) {
        throw new Error(
          "This sign-in link has expired or has already been used.",
        );
      }

      const user = await ConsoleUserRepository.FindEntryById(
        tokenRecord.user_id.toString(),
      );
      if (!user || !user.email) {
        throw new Error("The account for this link no longer exists.");
      }
      // Re-checked at redemption rather than trusted from when the link was sent:
      // access can be withdrawn between the mail going out and the click.
      if (!user.is_active || user.is_deleted) {
        throw new Error(
          "Your account is currently inactive. Please contact your administrator.",
        );
      }

      return await StartSession({ id: user.id, email: user.email }, entry);
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  const FindAllConsoleUsers = async (entry?: {
    page?: number;
    pageSize?: number;
    searchTerm?: string;
    excludeUserIds?: string[];
    includeUserIds?: string[];
    onlyActive?: boolean;
    isActive?: boolean;
    isSuperAdmin?: boolean;
    roleIds?: string[];
  }) => {
    try {
      if (entry?.includeUserIds && entry.includeUserIds.length === 0) {
        return {
          users: [],
          ...(entry?.page && entry?.pageSize
            ? {
                pagination: {
                  page: entry.page,
                  limit: entry.pageSize,
                  total: 0,
                  totalPages: 0,
                  hasNextPage: false,
                  hasPrevPage: false,
                },
              }
            : {}),
        };
      }
      const users = await ConsoleUserRepository.SearchEntries(entry);

      /*
       * Attach each user's roles.
       *
       * One query for the page, not one per row: the list shows role badges and
       * filters by role, and both need the same data.
       */
      const rows = (users as { users?: { id: string }[] })?.users ?? [];
      if (rows.length === 0) return users;

      const userIds = rows.map((user) => String(user.id));
      const [assignments, lastLogins] = await Promise.all([
        ConsoleUserRepository.FindRolesForUsers(userIds),
        ConsoleUserRepository.FindLastLoginForUsers(userIds),
      ]);
      const lastLoginByUser = new Map(
        lastLogins.map((row) => [
          String(row.console_user_id),
          row.last_login_at,
        ]),
      );
      const byUser = new Map<string, { id: string; name: string }[]>();
      for (const entryRow of assignments) {
        const list = byUser.get(String(entryRow.console_user_id)) ?? [];
        list.push({
          id: String(entryRow.role_id),
          name: entryRow.role_name,
        });
        byUser.set(String(entryRow.console_user_id), list);
      }

      return {
        ...(users as object),
        users: rows.map((user) => ({
          ...user,
          roles: byUser.get(String(user.id)) ?? [],
          // Derived from the sessions, not the (unwritten) column.
          last_login_at: lastLoginByUser.get(String(user.id)) ?? null,
        })),
      };
    } catch (err) {
      logError(err);
      throw new Error("Failed fetching console users");
    }
  };

  const UpdateConsoleUser = async (
    id: string,
    entry: {
      firstName?: string;
      lastName?: string;
      isActive?: boolean;
      isDeleted?: boolean;
      isSuperAdmin?: boolean;
      password?: string;
      roles?: {
        roleId: string;
      }[];
    },
  ) => {
    try {
      const currentUser = await ConsoleUserRepository.FindEntryById(id);
      let hashedPassword:
        | {
            hash: string;
            salt: string;
            iterations: number;
          }
        | undefined = undefined;
      if (entry.password) {
        hashedPassword = await hashPassword(entry.password);
      }
      const updated = await ConsoleUserRepository.UpdateEntry(id, {
        first_name: entry.firstName,
        last_name: entry.lastName,
        is_active: entry.isActive,
        is_delete: entry.isDeleted,
        isSuperAdmin: entry.isSuperAdmin,
        ...(hashedPassword
          ? {
              hashedPassword: hashedPassword.hash,
              passwordHashAlgo: HASH_ALGO,
              passwordIterations: hashedPassword.iterations,
              passwordSalt: hashedPassword.salt,
            }
          : {}),
      });
      const deletedUserRoles =
        await ConsoleUserRolesServices.DeleteUserRolesByUserId(updated.id);
      let updatedUserRoles: {
        created_at: Date;
        id: string;
        updated_at: Date | null;
        console_user_id: string;
        console_role_id: string;
      }[] = [];
      if (entry?.roles && entry?.roles?.length > 0 && !entry.isSuperAdmin) {
        updatedUserRoles = await ConsoleUserRolesServices.CreateUserRole(
          entry.roles.map((e) => ({
            userId: updated.id,
            roleId: e.roleId,
          })),
        );
      }

      const deletedRoleIds = new Set(
        deletedUserRoles?.map((r) => r.console_role_id) ?? [],
      );
      const updatedRoleIds = new Set(
        updatedUserRoles?.map((r) => r.console_role_id) ?? [],
      );
      const rolesChanged =
        deletedRoleIds.size !== updatedRoleIds.size ||
        [...deletedRoleIds].some((id) => !updatedRoleIds.has(id));
      if (
        rolesChanged ||
        currentUser.is_super_admin !== updated.is_super_admin ||
        hashedPassword
      ) {
        // remove console user sessions on update to avoid stale permissions in case of role/permission updates
        await ConsoleUserSessionServices.DeleteSessionByUserId(updated.id);
      }

      return updated;
    } catch (err) {
      logError(err);
      throw new Error("Failed updating user");
    }
  };

  const GetUserById = async (id: string) => {
    try {
      const userRecord = await ConsoleUserRepository.FindEntryById(id);
      return userRecord;
    } catch (err) {
      logError(err);
      throw new Error("Failed fetching user by id");
    }
  };

  const GetByEmail = async (email: string) => {
    try {
      const userRecord =
        await ConsoleUserRepository.FindEntryByEmailOrUsername(email);
      return userRecord;
    } catch (err) {
      logError(err);
      throw new Error("Failed fetching user by email");
    }
  };

  const GetSuperAdmins = async () => {
    try {
      const records = await ConsoleUserRepository.FindSuperAdmins();
      return records;
    } catch (err) {
      logError(err);
      throw new Error("Failed fetching super admins");
    }
  };

  const Me = async (id: string) => {
    try {
      const userRecord = await ConsoleUserRepository.FindEntryById(id);
      return userRecord;
    } catch (err) {
      logError(err);
      throw new Error("Failed fetching user by id");
    }
  };

  const SetPasswordByToken = async (entry: {
    token: string;
    password: string;
  }) => {
    try {
      const tokenRecord = await VerificationTokenServices.VerifyToken(
        entry.token,
        "console_user_invitation",
      );
      const isReset = !tokenRecord;
      const actualTokenRecord =
        tokenRecord ||
        (await VerificationTokenServices.VerifyToken(
          entry.token,
          "console_user_reset_password",
        ));

      if (!actualTokenRecord) {
        throw new Error("Invalid or expired token");
      }

      const hashedPassword = await hashPassword(entry.password);
      await ConsoleUserRepository.UpdateEntry(
        actualTokenRecord.user_id.toString(),
        {
          hashedPassword: hashedPassword.hash,
          passwordHashAlgo: HASH_ALGO,
          passwordIterations: hashedPassword.iterations,
          passwordSalt: hashedPassword.salt,
          is_active: true,
        },
      );

      await VerificationTokenServices.DeleteToken(
        actualTokenRecord.id.toString(),
      );
      return { success: true };
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  const RequestPasswordReset = async (email: string) => {
    try {
      const user =
        await ConsoleUserRepository.FindEntryByEmailOrUsername(email);
      if (!user) {
        return { success: true };
      }

      const { token } =
        await VerificationTokenServices.CreateResetPasswordToken(user.id);

      const resetLink = `${env.GetString("ADMIN_CONSOLE_URL")}/reset-password?token=${token}`;

      const mailOptions = await sendResetPasswordMail(
        {
          name: `${user.first_name} ${user.last_name}`,
          verification_link: resetLink,
        },
        { from: SMTP_FROM, to: user.email },
      );

      await transporter.sendMail(mailOptions);
      console.log(`[PasswordReset] Success! Email sent.`);

      return { success: true };
    } catch (err) {
      console.error(`[PasswordReset] CRITICAL ERROR:`, err);
      logError(err);
      throw err;
    }
  };

  return {
    RegisterNewUser,
    SigninConsoleUser,
    SsoSigninConsoleUser,
    FindAllConsoleUsers,
    UpdateConsoleUser,
    GetUserById,
    GetByEmail,
    GetSuperAdmins,
    Me,
    SetPasswordByToken,
    RequestPasswordReset,
  };
};

export type TConsoleUserServices = ReturnType<typeof ConsoleUserServices>;
