// src/services/user.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved

import assert from "node:assert";

import { MongooseBaseQueryOptions } from "mongoose";
import { v4 as uuidv4 } from "uuid";
import { filterText } from "dtp-cleantext";

import User from "../models/user.ts";
import { GadgetId, IUser, UserDocument } from "@gadget/api";

import ContactService from "./contact.ts";
import CryptoService from "./crypto.ts";

import { DtpService } from "../lib/service.js";
import { DtpPaginationParameters } from "../lib/pagination-parameters.ts";

export interface IUserUpdate {
  displayName?: string;
  currentPassword?: string;
  password?: string;
  persona?: string;
}

export interface IUserAdminUpdate {
  email: string;
  displayName?: string;
  password?: string;
  "flags.isEmailVerified": string;
  "flags.isAdmin": string;
  "flags.isTest": string;
  "flags.isBanned": string;
}
export interface IUserLibrary {
  users: IUser[];
  totalUserCount: number;
}

class UserService extends DtpService {
  get name(): string {
    return "UserService";
  }
  get slug(): string {
    return "user";
  }

  async start(): Promise<void> {}
  async stop(): Promise<void> {}

  async createDeferred(
    email: string,
    password: string,
    displayName?: string,
  ): Promise<UserDocument> {
    if (!email) {
      const error = new Error("must specify email address");
      error.statusCode = 400;
      throw error;
    }
    if (!password) {
      const error = new Error("must specify password");
      error.statusCode = 400;
      throw error;
    }

    if (displayName) {
      displayName = filterText(displayName);
      if (displayName.length < 3 || displayName.length > 30) {
        const err = new Error(
          "Display Name must be between 3-30 characters in length.",
        );
        err.statusCode = 400;
        throw err;
      }
    }

    /*
     * If we have an existing user, abort.
     */

    if (await this.getByEmail(email)) {
      const err = new Error("That email address is already registered.");
      err.statusCode = 400;
      throw err;
    }

    /*
     * Create a new User
     */

    const user = new User();
    user.email = filterText(email);
    user.email_lc = user.email.toLowerCase();

    user.passwordSalt = uuidv4();
    user.password = await CryptoService.maskPassword(
      user.passwordSalt,
      password.trim(),
    );

    if (displayName) {
      user.displayName = displayName;
    }

    user.flags = {
      isEmailVerified: false,
      isAdmin: false,
      isTest: false,
      isBanned: false,
    };

    return user;
  }

  async create(
    email: string,
    password: string,
    displayName?: string,
  ): Promise<IUser> {
    const user = await this.createDeferred(email, password, displayName);
    await user.save();

    await ContactService.sendVerificationEmail(user);

    return user.toObject();
  }

  async updateForUser(user: IUser, def: IUserUpdate): Promise<IUser> {
    const update: Record<string, unknown> = { $set: {} };
    const $set = update.$set as Record<string, unknown>;

    if (def.displayName !== undefined) {
      const displayName = filterText(def.displayName);
      if (displayName.length < 3 || displayName.length > 30) {
        const err = new Error(
          "Display Name must be between 3-30 characters in length.",
        );
        err.statusCode = 400;
        throw err;
      }
      $set.displayName = displayName;
      this.log.info("updating user display name", {
        user: { _id: user._id },
        displayName,
      });
    }

    if (def.persona !== undefined) {
      if (def.persona.length > 500) {
        const err = new Error(
          "Persona must be 500 characters or fewer.",
        );
        err.statusCode = 400;
        throw err;
      }
      $set.persona = def.persona;
      this.log.info("updating user persona", {
        user: { _id: user._id },
      });
    }

    if (def.password) {
      if (!def.currentPassword) {
        const err = new Error(
          "Current password is required to set a new password.",
        );
        err.statusCode = 400;
        throw err;
      }

      const existingUser = await User.findOne({ _id: user._id }).select(
        "+passwordSalt +password",
      );
      if (!existingUser) {
        const error = new Error(`User ${user._id} not found`);
        error.statusCode = 400;
        throw error;
      }

      const maskedCurrent = await CryptoService.maskPassword(
        existingUser.passwordSalt!,
        def.currentPassword.trim(),
      );
      if (maskedCurrent !== existingUser.password) {
        const err = new Error("Current password is incorrect.");
        err.statusCode = 400;
        throw err;
      }

      this.log.info("updating user password", { user: { _id: user._id } });
      $set.passwordSalt = uuidv4();
      $set.password = await CryptoService.maskPassword(
        $set.passwordSalt as string,
        def.password.trim(),
      );
    }

    if (Object.keys($set).length === 0) {
      return user;
    }

    const newUser = await User.findOneAndUpdate({ _id: user._id }, update, {
      new: true,
    });
    if (!newUser) {
      const error = new Error(`User ${user._id} not found`);
      error.statusCode = 400;
      throw error;
    }

    return newUser;
  }

  async updateForAdmin(user: IUser, def: IUserAdminUpdate): Promise<void> {
    const update: MongooseBaseQueryOptions = { $set: {} };

    if (def.email && def.email !== user.email) {
      update.$set.email = filterText(def.email);
      update.$set.email_lc = update.$set.email.toLowerCase();
      this.log.info("updating user email address", {
        user: { _id: user._id },
        email: update.$set.email,
      });
    }

    if (def.password) {
      this.log.info("updating user password", { user: { _id: user._id } });
      update.$set.passwordSalt = uuidv4();
      update.$set.password = await CryptoService.maskPassword(
        update.$set.passwordSalt,
        def.password.trim(),
      );
    }

    update.$set["flags.isEmailVerified"] =
      def["flags.isEmailVerified"] === "on";
    update.$set["flags.isAdmin"] = def["flags.isAdmin"] === "on";
    update.$set["flags.isTest"] = def["flags.isTest"] === "on";
    update.$set["flags.isBanned"] = def["flags.isBanned"] === "on";

    this.log.info("updating user flags", {
      user: { _id: user._id },
      flags: {
        isEmailVerified: update.$set["flags.isEmailVerified"],
        isAdmin: update.$set["flags.isAdmin"],
        isTest: update.$set["flags.isTest"],
        isBanned: update.$set["flags.isBanned"],
      },
    });

    await User.updateOne({ _id: user._id }, update);
  }

  async setEmailVerified(user: IUser, isVerified: boolean): Promise<IUser> {
    this.log.info("setting user email verified status", {
      user: { _id: user._id },
      isVerified,
    });

    const newUser = await User.findOneAndUpdate(
      { _id: user._id },
      { $set: { "flags.isEmailVerified": isVerified } },
      { new: true },
    );
    if (!newUser) {
      const error = new Error(`User ${user._id} not found`);
      error.statusCode = 400;
      throw error;
    }

    return newUser;
  }

  async getRecent(
    pagination: DtpPaginationParameters,
    query?: string,
  ): Promise<IUserLibrary> {
    const search: MongooseBaseQueryOptions = {};
    if (query) {
      this.log.debug("searching User", { query });
      search.$text = { $search: query };
    }
    const users = await User.find(search)
      .sort({ created: -1 })
      .skip(pagination.skip)
      .limit(pagination.cpp);
    const totalUserCount = await User.countDocuments(search);
    return { users, totalUserCount };
  }

  async getByEmail(email: string): Promise<IUser | undefined> {
    if (!email) {
      throw new Error("must specify email address");
    }
    const user = await User.findOne({ email_lc: email.trim().toLowerCase() });
    if (!user) {
      return;
    }
    return user;
  }

  async getById(userId: GadgetId): Promise<IUser> {
    if (!userId) {
      const error = new Error("must specify email address");
      error.statusCode = 400;
      throw error;
    }

    const user = await User.findOne({ _id: userId });
    if (!user) {
      const err = new Error(`User ${userId} not found`);
      err.statusCode = 400;
      throw err;
    }

    return user;
  }

  async authenticate(email: string, password: string): Promise<IUser> {
    if (!email) {
      const error = new Error("Must provide your email address");
      error.statusCode = 400;
      throw error;
    }
    if (!password) {
      const error = new Error("Must provide your password");
      error.statusCode = 400;
      throw error;
    }

    const emailLc = email.trim().toLowerCase();
    const user = await User.findOne({ email_lc: emailLc }).select(
      "+passwordSalt +password",
    );
    if (!user) {
      const error = new Error("Invalid email address or password");
      error.statusCode = 400;
      throw error;
    }
    assert(user.password && user.passwordSalt, "user authentication failed");

    const maskedPassword = await CryptoService.maskPassword(
      user.passwordSalt,
      password.trim(),
    );
    if (maskedPassword !== user.password) {
      const error = new Error("Invalid email address or password");
      error.statusCode = 400;
      throw error;
    }

    delete user.passwordSalt;
    delete user.password;
    return user;
  }
}

export default new UserService();
