import { Injectable, UnauthorizedException, Logger } from "@nestjs/common";
import type { CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";

import type { Request } from "express";

import { TokenBlacklistService } from "../../../infra/token/token-blacklist.service.js";
import {
  IS_PUBLIC_KEY,
  IS_PUBLIC_NO_SESSION_KEY,
  IS_OPTIONAL_AUTH_KEY,
} from "../../../decorators/public.decorator.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { JoseJwtService } from "../../../infra/token/jose-jwt.service.js";
import { UsersService } from "@bff/modules/users/application/users.service.js";
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js";
import type { UserAuth } from "@customer-portal/domain/customer";
import { extractAccessTokenFromRequest } from "../../../utils/token-from-request.util.js";

type CookieValue = string | undefined;
type RequestBase = Omit<Request, "cookies" | "route">;
type RequestWithCookies = RequestBase & {
  cookies?: Record<string, CookieValue>;
};
type RequestWithRoute = RequestWithCookies & {
  method: string;
  url: string;
  route?: { path?: string };
};

@Injectable()
export class GlobalAuthGuard implements CanActivate {
  private readonly logger = new Logger(GlobalAuthGuard.name);

  constructor(
    private reflector: Reflector,
    private readonly tokenBlacklistService: TokenBlacklistService,
    private readonly jwtService: JoseJwtService,
    private readonly usersService: UsersService
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = this.getRequest(context);
    const route = `${request.method} ${request.url}`;
    const isLogoutRoute = this.isLogoutRoute(request);

    // Check if the route is marked as public
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    const isPublicNoSession = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_NO_SESSION_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) {
      if (!isPublicNoSession) {
        // Try to attach user from token if present (optional)
        const token = extractAccessTokenFromRequest(request);
        if (token) {
          try {
            await this.attachUserFromToken(request, token);
          } catch {
            // Public endpoints should remain accessible even if the session is missing/expired/invalid
          }
        }
      }
      return true;
    }

    // Check if the route is marked as optionally authenticated
    // OptionalAuth: no token = allow (user=null), invalid token = 401
    const isOptionalAuth = this.reflector.getAllAndOverride<boolean>(IS_OPTIONAL_AUTH_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isOptionalAuth) {
      const token = extractAccessTokenFromRequest(request);
      if (!token) {
        // No token = not logged in, allow request with no user attached
        return true;
      }

      // Token present - validate it strictly (invalid token = 401)
      await this.attachUserFromToken(request, token, route);
      return true;
    }

    try {
      const token = extractAccessTokenFromRequest(request);
      if (!token) {
        if (isLogoutRoute) {
          return true;
        }
        throw new UnauthorizedException("Missing token");
      }

      await this.attachUserFromToken(request, token, route);
      return true;
    } catch (error) {
      if (error instanceof UnauthorizedException) {
        if (isLogoutRoute) {
          return true;
        }
        // Only log warnings for requests with an invalid token (not missing tokens)
        const token = extractAccessTokenFromRequest(request);
        if (typeof token === "string") {
          this.logger.warn(`Unauthorized access attempt to ${route}`);
        }
      } else {
        this.logger.error(`Authentication error for route ${route}: ${extractErrorMessage(error)}`);
      }
      throw error;
    }
  }
  getRequest(context: ExecutionContext): RequestWithRoute {
    const rawRequest = context.switchToHttp().getRequest<unknown>();
    if (!this.isRequestWithRoute(rawRequest)) {
      this.logger.error("Unable to determine HTTP request in auth guard");
      throw new UnauthorizedException("Invalid request context");
    }
    return rawRequest;
  }

  private isRequestWithRoute(value: unknown): value is RequestWithRoute {
    if (!value || typeof value !== "object") {
      return false;
    }
    const candidate = value as Partial<RequestWithRoute>;
    return (
      typeof candidate.method === "string" &&
      typeof candidate.url === "string" &&
      (!candidate.route || typeof candidate.route === "object")
    );
  }

  private isLogoutRoute(request: RequestWithRoute): boolean {
    if (!request || typeof request.method !== "string" || typeof request.url !== "string") {
      return false;
    }

    if (request.method.toUpperCase() !== "POST") {
      return false;
    }

    const routePath = request.route?.path;
    if (routePath && this.isLogoutPath(routePath)) {
      return true;
    }

    const normalizedUrl = request.url.split("?")[0] ?? "";
    return this.isLogoutPath(normalizedUrl);
  }

  private isLogoutPath(path: string): boolean {
    if (typeof path !== "string" || path.length === 0) {
      return false;
    }

    const normalized = path.endsWith("/") ? path.slice(0, -1) : path;
    return normalized === "/auth/logout" || normalized === "/api/auth/logout";
  }

  private async attachUserFromToken(
    request: RequestWithRoute,
    token: string,
    route?: string
  ): Promise<void> {
    const payload = await this.jwtService.verify<{ sub?: string; email?: string; exp?: number }>(
      token
    );

    const tokenType = (payload as { type?: unknown }).type;
    if (typeof tokenType === "string" && tokenType !== "access") {
      throw new UnauthorizedException("Invalid access token");
    }

    if (!payload.sub || !payload.email) {
      throw new UnauthorizedException("Invalid token payload");
    }

    // Explicit expiry buffer check to avoid tokens expiring mid-request
    if (typeof payload.exp !== "number") {
      throw new UnauthorizedException("Token missing expiration claim");
    }
    const nowSeconds = Math.floor(Date.now() / 1000);
    if (payload.exp < nowSeconds + 60) {
      throw new UnauthorizedException("Token expired or expiring soon");
    }

    // Then check token blacklist
    const isBlacklisted = await this.tokenBlacklistService.isTokenBlacklisted(token);
    if (isBlacklisted) {
      if (route) {
        this.logger.warn(`Blacklisted token attempted access to: ${route}`);
      }
      throw new UnauthorizedException("Token has been revoked");
    }

    const prismaUser = await this.usersService.findByIdInternal(payload.sub);
    if (!prismaUser) {
      throw new UnauthorizedException("User not found");
    }
    if (prismaUser.email !== payload.email) {
      throw new UnauthorizedException("Token subject does not match user record");
    }

    const profile: UserAuth = mapPrismaUserToDomain(prismaUser);
    (request as RequestWithRoute & { user?: UserAuth }).user = profile;
  }
}
