// src/services/auth.ts
// Copyright (C) 2026 DTP Technologies, LLC
// All Rights Reserved

import jwt from "jsonwebtoken";

import { DtpService } from "../lib/service.js";
import env from "../config/env.js";
import UserService from "./user.js";
import SessionService from "./session.js";
import { IUser } from "../models/user.js";
import { ISession } from "../models/session.js";

export interface JwtPayload {
  userId: string;
  username: string;
  sessionId: string;
}

export interface AuthTokens {
  accessToken: string;
  expiresIn: string;
}

export interface AuthResult {
  success: boolean;
  user: IUser;
  session: ISession;
  tokens: AuthTokens;
}

export class AuthService extends DtpService {
  get name(): string {
    return "AuthService";
  }
  get slug(): string {
    return "auth";
  }

  private jwtSecret: string;
  private jwtExpiresIn: number = 24 * 60 * 60; // 24 hours in seconds
  private jwtRefreshThreshold: number = 20; // Refresh when < 20 hours remaining

  constructor() {
    super();
    const secret = env.user.jwtSecret;
    if (!secret) {
      throw new Error(
        "JWT secret not configured. Set environmentSalt or JWT_SECRET in config.",
      );
    }
    this.jwtSecret = secret;
  }

  async start(): Promise<void> {
    this.log.info("service started", {
      jwtExpiresIn: this.jwtExpiresIn,
      refreshThreshold: this.jwtRefreshThreshold,
    });
  }

  async stop(): Promise<void> {
    this.log.info("service stopped");
  }

  /**
   * Authenticate user with username and password
   */
  async authenticate(
    username: string,
    password: string,
    ipAddress?: string,
    userAgent?: string,
  ): Promise<AuthResult> {
    this.log.info("authenticate() called", { username, ipAddress, userAgent });

    // Validate input
    if (!username || !password) {
      this.log.error("authenticate() failed - missing credentials", {
        hasUsername: !!username,
        hasPassword: !!password,
      });
      throw new Error("Username and password are required");
    }

    this.log.debug("authenticate() - looking up user", { username });

    // Find user with credentials
    const user = await UserService.getUserByUsernameWithCredentials(username);

    if (!user) {
      this.log.error("authenticate() failed - user not found", { username });
      throw new Error("Invalid username or password");
    }

    this.log.debug("authenticate() - user found, verifying password", {
      userId: user._id,
      username: user.username,
    });

    // Verify password
    const isValid = await UserService.verifyPassword(user, password);
    if (!isValid) {
      this.log.error("authenticate() failed - invalid password", {
        userId: user._id,
        username: user.username,
      });
      throw new Error("Invalid username or password");
    }

    this.log.debug("authenticate() - password valid, checking ban status", {
      userId: user._id,
      username: user.username,
    });

    // Check if user is banned
    if (UserService.isUserBanned(user)) {
      this.log.error("authenticate() failed - user is banned", {
        userId: user._id,
        username: user.username,
      });
      throw new Error("Account has been suspended");
    }

    this.log.debug(
      "authenticate() - user not banned, revoking existing sessions",
      {
        userId: user._id,
        username: user.username,
      },
    );

    // Revoke any existing sessions (single session policy)
    await SessionService.revokeAllForUser(user._id.toString());
    this.log.debug("authenticate() - existing sessions revoked", {
      userId: user._id,
      username: user.username,
    });

    // Create new session
    this.log.debug("authenticate() - creating new session", {
      userId: user._id,
      username: user.username,
      ipAddress,
      userAgent,
    });
    const session = await SessionService.create(
      user._id.toString(),
      ipAddress,
      userAgent,
    );
    this.log.debug("authenticate() - session created", {
      sessionId: session._id,
      userId: user._id,
      username: user.username,
    });

    // Generate JWT token
    this.log.debug("authenticate() - generating JWT token", {
      sessionId: session._id,
      userId: user._id,
      username: user.username,
    });
    const tokens = this.generateJwtToken(user, session);
    this.log.debug("authenticate() - JWT token generated", {
      sessionId: session._id,
      userId: user._id,
      username: user.username,
      expiresIn: tokens.expiresIn,
      accessTokenLength: tokens.accessToken.length,
    });

    this.log.info("User authenticated successfully", {
      userId: user._id,
      username: user.username,
      sessionId: session._id,
      tokenExpiresIn: tokens.expiresIn,
    });

    return {
      success: true,
      user,
      session,
      tokens,
    };
  }

  /**
   * Generate JWT token for user
   */
  generateJwtToken(user: IUser, session: ISession): AuthTokens {
    this.log.debug("generateJwtToken() - creating JWT", {
      userId: user._id,
      username: user.username,
      sessionId: session._id,
      expiresIn: this.jwtExpiresIn,
    });

    const payload: JwtPayload = {
      userId: user._id.toString(),
      username: user.username,
      sessionId: session._id.toString(),
    };

    const accessToken = jwt.sign(payload, this.jwtSecret, {
      expiresIn: this.jwtExpiresIn,
    });

    this.log.debug("generateJwtToken() - JWT created", {
      sessionId: session._id,
      tokenLength: accessToken.length,
      first20Chars: accessToken.substring(0, 20) + "...",
    });

    return {
      accessToken,
      expiresIn: `${this.jwtExpiresIn}s`,
    };
  }

  /**
   * Verify JWT token and return payload
   */
  verifyToken(token: string): JwtPayload | null {
    this.log.debug("verifyToken() - verifying token", {
      tokenLength: token.length,
      first20Chars: token.substring(0, 20) + "...",
    });

    try {
      const decoded = jwt.verify(token, this.jwtSecret) as JwtPayload;
      this.log.debug("verifyToken() - token verified successfully", {
        userId: decoded.userId,
        username: decoded.username,
        sessionId: decoded.sessionId,
      });
      return decoded;
    } catch (error) {
      const err = error as Error;
      this.log.error("verifyToken() - token verification failed", {
        errorName: err.name,
        errorMessage: err.message,
        tokenLength: token.length,
      });
      return null;
    }
  }

  /**
   * Check if token needs refresh (returns true if should refresh)
   */
  shouldRefreshToken(token: string): boolean {
    this.log.debug("shouldRefreshToken() - checking token", {
      tokenLength: token.length,
      refreshThreshold: this.jwtRefreshThreshold,
    });

    try {
      const decoded = jwt.decode(token) as jwt.JwtPayload & JwtPayload;
      if (!decoded || !decoded.exp) {
        this.log.debug("shouldRefreshToken() - no exp claim, not refreshing");
        return false;
      }

      const now = Math.floor(Date.now() / 1000);
      const remainingSeconds = decoded.exp - now;
      const remainingHours = remainingSeconds / 3600;

      this.log.debug("shouldRefreshToken() - token age analysis", {
        exp: decoded.exp,
        now,
        remainingSeconds,
        remainingHours,
        shouldRefresh: remainingHours < this.jwtRefreshThreshold,
      });

      return remainingHours < this.jwtRefreshThreshold;
    } catch (error) {
      const err = error as Error;
      this.log.error("shouldRefreshToken() - decode failed", {
        errorMessage: err.message,
      });
      return false;
    }
  }

  /**
   * Refresh JWT token
   */
  async refreshToken(currentToken: string): Promise<AuthTokens | null> {
    this.log.info("refreshToken() - called", {
      tokenLength: currentToken.length,
      first20Chars: currentToken.substring(0, 20) + "...",
    });

    const payload = this.verifyToken(currentToken);
    if (!payload) {
      this.log.error("refreshToken() - verifyToken failed, returning null");
      return null;
    }

    this.log.debug("refreshToken() - token verified, checking session", {
      sessionId: payload.sessionId,
      userId: payload.userId,
    });

    // Validate session still exists
    const session = await SessionService.findByToken(payload.sessionId);
    if (!session) {
      this.log.warn("refreshToken() - session not found", {
        sessionId: payload.sessionId,
        userId: payload.userId,
      });
      return null;
    }

    this.log.debug("refreshToken() - session found, fetching user", {
      sessionId: session._id,
      userId: payload.userId,
    });

    // Get user
    const user = await UserService.getUserByIdWithCredentials(
      new (await import("mongoose")).Types.ObjectId(payload.userId),
    );
    if (!user) {
      this.log.error("refreshToken() - user not found", {
        userId: payload.userId,
        sessionId: payload.sessionId,
      });
      return null;
    }

    this.log.debug("refreshToken() - user found, extending session", {
      userId: user._id,
      sessionId: session._id,
    });

    // Extend session
    await SessionService.extend(session);
    this.log.debug("refreshToken() - session extended", {
      sessionId: session._id,
      userId: user._id,
    });

    // Generate new token
    this.log.info("refreshToken() - generating new token", {
      sessionId: session._id,
      userId: user._id,
    });
    const tokens = this.generateJwtToken(user, session);
    this.log.info("refreshToken() - token refreshed successfully", {
      sessionId: session._id,
      userId: user._id,
      expiresIn: tokens.expiresIn,
    });

    return tokens;
  }

  /**
   * Logout user (revoke session)
   */
  async logout(token: string): Promise<boolean> {
    this.log.info("logout() - called", {
      tokenLength: token.length,
      first20Chars: token.substring(0, 20) + "...",
    });

    const payload = this.verifyToken(token);
    if (!payload) {
      this.log.warn("logout() - invalid token, returning false");
      return false;
    }

    this.log.debug("logout() - token valid, revoking session", {
      sessionId: payload.sessionId,
      userId: payload.userId,
    });

    const revoked = await SessionService.revoke(payload.sessionId);
    if (revoked) {
      this.log.info("logout() - session revoked successfully", {
        sessionId: payload.sessionId,
        userId: payload.userId,
      });
    } else {
      this.log.warn("logout() - session revoke failed", {
        sessionId: payload.sessionId,
        userId: payload.userId,
      });
    }
    return revoked;
  }

  /**
   * Get cookie options for JWT
   */
  getCookieOptions() {
    const cookieDomain = env.web.cookieDomain;

    this.log.debug("getCookieOptions() - returning cookie config", {
      httpOnly: true,
      secure: true,
      sameSite: "lax",
      maxAge: 24 * 60 * 60 * 1000,
      domain: cookieDomain || "(none - default to request host)",
    });

    return {
      httpOnly: true,
      secure: true, // Always secure - both prod and dev use HTTPS
      sameSite: "lax" as const, // Allow cross-site for dev proxy
      maxAge: 24 * 60 * 60 * 1000, // 24 hours
      ...(cookieDomain ? { domain: cookieDomain } : {}), // Only set domain if configured
    };
  }

  /**
   * Generate Set-Cookie header to clear auth_token cookie
   * This clears ALL duplicate cookies by setting an expired date
   */
  getClearCookieHeader(): string {
    const options = this.getCookieOptions();
    const expires = "Thu, 01 Jan 1970 00:00:00 GMT";

    this.log.debug("getClearCookieHeader() - generating clear cookie header", {
      domain: options.domain,
      expires,
    });

    const parts = ["auth_token=", "Expires=" + expires, "Max-Age=0", "Path=/"];

    if (options.domain) {
      parts.push("Domain=" + options.domain);
    }
    if (options.httpOnly) {
      parts.push("HttpOnly");
    }
    if (options.secure) {
      parts.push("Secure");
    }
    if (options.sameSite) {
      parts.push("SameSite=" + options.sameSite);
    }

    const header = parts.join("; ");
    this.log.debug("getClearCookieHeader() - generated header", {
      headerLength: header.length,
      header,
    });

    return header;
  }
}

export default new AuthService();
