import { Injectable, Inject } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import * as crypto from "crypto";

export interface CsrfTokenData {
  token: string;
  secret: string;
  expiresAt: Date;
  sessionId?: string;
  userId?: string;
}

export interface CsrfValidationResult {
  isValid: boolean;
  reason?: string;
}

export interface CsrfTokenStats {
  mode: "stateless";
  totalTokens: number;
  activeTokens: number;
  expiredTokens: number;
  cacheSize: number;
  maxCacheSize: number;
}

/**
 * Service for CSRF token generation and validation using deterministic HMAC tokens.
 */
@Injectable()
export class CsrfService {
  private readonly tokenExpiry: number;

  private readonly secretKey: string;

  private readonly cookieName: string;

  private readonly headerName: string;

  constructor(
    private readonly configService: ConfigService,
    @Inject(Logger) private readonly logger: Logger
  ) {
    this.tokenExpiry = Number(this.configService.get("CSRF_TOKEN_EXPIRY", "3600000"));
    this.secretKey = this.configService.get("CSRF_SECRET_KEY") || this.generateSecretKey();
    this.cookieName = this.configService.get("CSRF_COOKIE_NAME", "csrf-secret");
    this.headerName = this.configService.get("CSRF_HEADER_NAME", "X-CSRF-Token");

    if (!this.configService.get("CSRF_SECRET_KEY")) {
      this.logger.warn(
        "CSRF_SECRET_KEY not configured, using ephemeral key (not suitable for production)"
      );
    }
  }

  getCookieName(): string {
    return this.cookieName;
  }

  getHeaderName(): string {
    return this.headerName;
  }

  getTokenTtl(): number {
    return this.tokenExpiry;
  }

  generateToken(
    existingSecret?: string | null,
    sessionId?: string,
    userId?: string
  ): CsrfTokenData {
    const issuedAt = Date.now();
    const secret = existingSecret ?? this.generateSecret();
    const token = this.signToken(secret, sessionId, userId, issuedAt);
    const expiresAt = new Date(issuedAt + this.tokenExpiry);

    this.logger.debug("CSRF token generated", {
      tokenHash: this.hashToken(token),
      sessionId,
      userId,
      expiresAt: expiresAt.toISOString(),
      reusedSecret: Boolean(existingSecret),
    });

    return {
      token,
      secret,
      expiresAt,
      sessionId,
      userId,
    };
  }

  validateToken(
    token: string,
    secret: string,
    sessionId?: string,
    userId?: string
  ): CsrfValidationResult {
    if (!token || !secret) {
      return { isValid: false, reason: "Missing token or secret" };
    }

    const parsed = this.parseToken(token);
    if (!parsed) {
      this.logger.warn("CSRF token validation failed - malformed token", {
        tokenHash: this.hashToken(token),
      });
      return { isValid: false, reason: "Malformed token" };
    }

    const { issuedAt } = parsed;
    if (Date.now() > issuedAt + this.tokenExpiry) {
      return { isValid: false, reason: "Token expired" };
    }

    const expectedToken = this.signToken(secret, sessionId, userId, issuedAt);
    if (!this.constantTimeEquals(token, expectedToken)) {
      this.logger.warn("CSRF token validation failed - signature mismatch", {
        tokenHash: this.hashToken(token),
        sessionId,
        userId,
      });
      return { isValid: false, reason: "Invalid token" };
    }

    return { isValid: true };
  }

  invalidateToken(_token: string): void {
    // Stateless tokens are tied to the secret cookie; rotate cookie to invalidate.
    this.logger.debug("invalidateToken called for stateless CSRF token");
  }

  invalidateSessionTokens(_sessionId: string): void {
    this.logger.debug("invalidateSessionTokens called - rotate cookie to enforce");
  }

  invalidateUserTokens(_userId: string): void {
    this.logger.debug("invalidateUserTokens called - rotate cookie to enforce");
  }

  getTokenStats(): CsrfTokenStats {
    return {
      mode: "stateless",
      totalTokens: 0,
      activeTokens: 0,
      expiredTokens: 0,
      cacheSize: 0,
      maxCacheSize: 0,
    };
  }

  private generateSecret(): string {
    return crypto.randomBytes(32).toString("base64url");
  }

  private signToken(
    secret: string,
    sessionId: string | undefined,
    userId: string | undefined,
    issuedAt: number
  ): string {
    const payload = [issuedAt.toString(), secret, sessionId || "", userId || ""].join(":");
    const hmac = crypto.createHmac("sha256", this.secretKey).update(payload).digest("base64url");
    return `${issuedAt}.${hmac}`;
  }

  private parseToken(token: string): { issuedAt: number } | null {
    const [issuedAtRaw, signature] = token.split(".");
    if (!issuedAtRaw || !signature) {
      return null;
    }

    const issuedAt = Number(issuedAtRaw);
    if (!Number.isFinite(issuedAt)) {
      return null;
    }

    return { issuedAt };
  }

  private generateSecretKey(): string {
    const key = crypto.randomBytes(64).toString("base64url");
    this.logger.warn(
      "Generated CSRF secret key - set CSRF_SECRET_KEY environment variable for production"
    );
    return key;
  }

  private hashToken(token: string): string {
    return crypto.createHash("sha256").update(token).digest("hex").substring(0, 16);
  }

  private constantTimeEquals(a: string, b: string): boolean {
    if (a.length !== b.length) {
      return false;
    }

    let result = 0;
    for (let i = 0; i < a.length; i++) {
      result |= a.charCodeAt(i) ^ b.charCodeAt(i);
    }

    return result === 0;
  }
}
