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

import env from "../config/env.js";

import { Request } from "express";

import jwt from "jsonwebtoken";
import dayjs from "dayjs";
import geoip from "geoip-lite";

import { GadgetId, IUser } from "@gadget/api";
import { IWebToken, WebToken } from "../models/web-token.js";
import { WebVisit } from "../models/web-visit.js";

import UserService from "../services/user.js";
import { DtpService } from "../lib/service.js";
import { PopulateOptions } from "mongoose";

interface UserWebToken {
  _id: string;
  email: string;
  displayName: string;
  webToken: IWebToken | GadgetId;
}

class SessionService extends DtpService {
  private populateWebToken: PopulateOptions[] = [
    {
      path: "user",
      select: "-passwordSalt -password",
    },
  ];

  get name(): string {
    return "SessionService";
  }
  get slug(): string {
    return "session";
  }

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

  async createJsonWebToken(user: IUser): Promise<string> {
    const NOW = new Date();
    const webToken = new WebToken();
    webToken.created = NOW;
    webToken.expires = dayjs(NOW).add(7, "day").toDate();
    webToken.user = user._id;

    const payload: UserWebToken = {
      _id: user._id,
      email: user.email,
      displayName: user.displayName,
      webToken: webToken._id,
    };
    const token = jwt.sign(payload, env.auth.jwtSecret, {
      expiresIn: "24h",
    });

    webToken.token = token;
    await webToken.save();

    return token;
  }

  async verifyJsonWebToken(token: string): Promise<IUser> {
    try {
      const NOW = new Date();
      const payload = jwt.verify(token, env.auth.jwtSecret) as UserWebToken;
      const userId = payload._id;

      const webToken = await WebToken.findOne({
        _id: payload.webToken,
      }).populate(this.populateWebToken);
      if (!webToken) {
        const error = new Error("Invalid JSON Web Token");
        error.name = "InvalidToken";
        error.statusCode = 401;
        throw error;
      }

      if (webToken.expires < NOW) {
        await WebToken.deleteOne({ _id: webToken._id });
        const error = new Error("JSON Web Token has expired");
        error.name = "ExpiredToken";
        error.statusCode = 401;
        throw error;
      }
      if (userId !== (webToken.user as IUser)._id) {
        const error = new Error("JSON Web Token ownership mismatch");
        error.name = "TokenOwnershipMismatch";
        error.statusCode = 401;
        throw error;
      }

      const user = await UserService.getById(userId);
      return user;
    } catch (cause) {
      // this.log.error("failed to verify JSON Web Token", {
      //   token,
      //   error: cause,
      // });
      const error = new Error("Invalid JSON Web Token", { cause });
      error.name = "TokenVerifyError";
      error.statusCode = 401;
      throw error;
    }
  }

  async revokeJsonWebToken(token: string): Promise<void> {
    const payload = jwt.verify(token, env.auth.jwtSecret) as UserWebToken;
    this.log.info("revoking JSON Web Token", { tokenId: payload.webToken });
    await WebToken.deleteOne({ _id: payload.webToken });
  }

  async recordWebVisit(req: Request): Promise<void> {
    const NOW = new Date();
    const visit = new WebVisit();
    visit.created = NOW;
    visit.url = req.url;
    if (req.user) {
      visit.user = req.user._id;
    }
    visit.userAgent = req.headers["user-agent"];
    visit.referrer = req.headers["referer"];
    visit.ipAddress = req.ip;

    if (req.ip) {
      const ipLookup = geoip.lookup(req.ip);
      if (ipLookup) {
        visit.country = ipLookup.country;
        visit.region = ipLookup.region;
        visit.eu = ipLookup.eu === "1";
        visit.timezone = ipLookup.timezone;
        visit.city = ipLookup.city;
        visit.latitude = ipLookup.ll[0];
        visit.longitude = ipLookup.ll[1];
        visit.metroCode = ipLookup.metro;
      }
    }

    await visit.save();
  }
}

export default new SessionService();
