import { randomUUID } from "crypto";

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

import type {
  GetStartedSession,
  AccountStatus,
  GuestHandoffToken,
} from "@customer-portal/domain/get-started";

import { CacheService } from "@/infra/cache/cache.service.js";
import { DistributedLockService } from "@/infra/cache/distributed-lock.service.js";

/**
 * Session data stored in Redis (internal representation)
 */
interface SessionData extends Omit<GetStartedSession, "expiresAt"> {
  /** Session ID (for lookup) */
  id: string;
  /** Timestamp when session was marked as used (for one-time operations) */
  usedAt?: string;
  /** The operation that used this session */
  usedFor?: "signup_with_eligibility" | "complete_account" | "migrate_whmcs_account";
}

/**
 * Build a clean SessionData object from partial data, filtering out undefined values.
 * This maintains exactOptionalPropertyTypes compliance.
 */
function buildSessionData(
  required: Pick<SessionData, "id" | "email" | "emailVerified" | "createdAt">,
  optional: Partial<Omit<SessionData, "id" | "email" | "emailVerified" | "createdAt">>
): SessionData {
  const result: SessionData = { ...required };

  if (optional.usedAt !== undefined) result.usedAt = optional.usedAt;
  if (optional.usedFor !== undefined) result.usedFor = optional.usedFor;
  if (optional.accountStatus !== undefined) result.accountStatus = optional.accountStatus;
  if (optional.firstName !== undefined) result.firstName = optional.firstName;
  if (optional.lastName !== undefined) result.lastName = optional.lastName;
  if (optional.phone !== undefined) result.phone = optional.phone;
  if (optional.address !== undefined) result.address = optional.address;
  if (optional.sfAccountId !== undefined) result.sfAccountId = optional.sfAccountId;
  if (optional.whmcsClientId !== undefined) result.whmcsClientId = optional.whmcsClientId;
  if (optional.eligibilityStatus !== undefined)
    result.eligibilityStatus = optional.eligibilityStatus;

  return result;
}

/**
 * Get Started Session Service
 *
 * Manages temporary sessions for the unified "Get Started" flow.
 * Sessions track progress through email verification, account detection,
 * and account completion.
 *
 * Security:
 * - Sessions are stored in Redis with automatic TTL
 * - Session tokens are UUIDs (unguessable)
 * - Sessions are invalidated after account creation
 */
@Injectable()
export class GetStartedSessionService {
  private readonly SESSION_PREFIX = "get-started-session:";
  private readonly HANDOFF_PREFIX = "guest-handoff:";
  private readonly SESSION_LOCK_PREFIX = "session-lock:";
  private readonly ttlSeconds: number;
  /** TTL for handoff tokens (30 minutes) */
  private readonly handoffTtlSeconds = 1800;
  /** Lock TTL must exceed workflow lock (60s) to prevent premature expiry */
  private readonly sessionLockTtlMs = 65_000;

  constructor(
    private readonly cache: CacheService,
    private readonly lockService: DistributedLockService,
    private readonly config: ConfigService,
    @Inject(Logger) private readonly logger: Logger
  ) {
    this.ttlSeconds = this.config.get<number>("GET_STARTED_SESSION_TTL", 3600);
  }

  /**
   * Create a new session for email verification
   */
  async create(email: string): Promise<string> {
    const sessionId = randomUUID();
    const normalizedEmail = email.toLowerCase().trim();

    const sessionData: SessionData = {
      id: sessionId,
      email: normalizedEmail,
      emailVerified: false,
      createdAt: new Date().toISOString(),
    };

    await this.cache.set(this.buildKey(sessionId), sessionData, this.ttlSeconds);
    this.logger.debug({ email: normalizedEmail, sessionId }, "Get-started session created");

    return sessionId;
  }

  /**
   * Get session by token
   */
  async get(sessionToken: string): Promise<GetStartedSession | null> {
    const sessionData = await this.cache.get<SessionData>(this.buildKey(sessionToken));
    if (!sessionData) return null;

    return {
      ...sessionData,
      expiresAt: this.calculateExpiresAt(sessionData.createdAt),
    };
  }

  /**
   * Update session with email verification status and optional prefill data
   */
  async markEmailVerified(
    sessionToken: string,
    accountStatus: AccountStatus,
    prefillData?: {
      firstName?: string;
      lastName?: string;
      phone?: string;
      address?: GetStartedSession["address"];
      sfAccountId?: string;
      whmcsClientId?: number;
      eligibilityStatus?: string;
    }
  ): Promise<boolean> {
    const sessionData = await this.cache.get<SessionData>(this.buildKey(sessionToken));
    if (!sessionData) return false;

    // Use buildSessionData to properly filter undefined values
    const updatedData = buildSessionData(
      {
        id: sessionData.id,
        email: sessionData.email,
        emailVerified: true,
        createdAt: sessionData.createdAt,
      },
      {
        accountStatus,
        firstName: prefillData?.firstName,
        lastName: prefillData?.lastName,
        phone: prefillData?.phone,
        address: prefillData?.address,
        sfAccountId: prefillData?.sfAccountId,
        whmcsClientId: prefillData?.whmcsClientId,
        eligibilityStatus: prefillData?.eligibilityStatus,
      }
    );

    const remainingTtl = this.calculateRemainingTtl(sessionData.createdAt);
    await this.cache.set(this.buildKey(sessionToken), updatedData, remainingTtl);

    this.logger.debug(
      { sessionId: sessionToken, accountStatus },
      "Get-started session marked as verified"
    );

    return true;
  }

  /**
   * Delete session (after successful account creation)
   */
  async invalidate(sessionToken: string): Promise<void> {
    await this.cache.del(this.buildKey(sessionToken));
    this.logger.debug({ sessionId: sessionToken }, "Get-started session invalidated");
  }

  /**
   * Validate that a session exists and email is verified
   */
  async validateVerifiedSession(sessionToken: string): Promise<GetStartedSession | null> {
    const session = await this.get(sessionToken);

    if (!session) {
      this.logger.warn({ sessionId: sessionToken }, "Session not found");
      return null;
    }

    if (!session.emailVerified) {
      this.logger.warn({ sessionId: sessionToken }, "Session email not verified");
      return null;
    }

    return session;
  }

  // ============================================================================
  // Guest Handoff Token Methods
  // ============================================================================

  /**
   * Create a guest handoff token for eligibility-to-account-creation flow
   *
   * This token allows passing data from guest eligibility check to account creation
   * without requiring email verification first. The email will be verified when
   * the user proceeds to account creation.
   */
  async createGuestHandoffToken(
    email: string,
    data: {
      firstName: string;
      lastName: string;
      address: GetStartedSession["address"];
      phone?: string;
      sfAccountId: string;
    }
  ): Promise<string> {
    const tokenId = randomUUID();
    const normalizedEmail = email.toLowerCase().trim();

    const tokenData: GuestHandoffToken = {
      id: tokenId,
      type: "guest_handoff",
      email: normalizedEmail,
      emailVerified: false,
      firstName: data.firstName,
      lastName: data.lastName,
      address: data.address,
      phone: data.phone,
      sfAccountId: data.sfAccountId,
      createdAt: new Date().toISOString(),
    };

    await this.cache.set(this.buildHandoffKey(tokenId), tokenData, this.handoffTtlSeconds);
    this.logger.debug({ email: normalizedEmail, tokenId }, "Guest handoff token created");

    return tokenId;
  }

  /**
   * Retrieve and validate a guest handoff token
   *
   * Returns the token data if valid, null otherwise.
   * The token email must match the verified email for security.
   */
  async getGuestHandoffToken(
    tokenId: string,
    verifiedEmail: string
  ): Promise<GuestHandoffToken | null> {
    const tokenData = await this.cache.get<GuestHandoffToken>(this.buildHandoffKey(tokenId));

    if (!tokenData) {
      this.logger.debug({ tokenId }, "Guest handoff token not found or expired");
      return null;
    }

    // Security: Ensure the token email matches the verified email
    const normalizedVerifiedEmail = verifiedEmail.toLowerCase().trim();
    if (tokenData.email !== normalizedVerifiedEmail) {
      this.logger.warn(
        { tokenId, tokenEmail: tokenData.email, verifiedEmail: normalizedVerifiedEmail },
        "Guest handoff token email mismatch - possible security issue"
      );
      return null;
    }

    this.logger.debug({ tokenId, email: normalizedVerifiedEmail }, "Guest handoff token retrieved");
    return tokenData;
  }

  /**
   * Delete a guest handoff token after it has been used
   */
  async invalidateHandoffToken(tokenId: string): Promise<void> {
    await this.cache.del(this.buildHandoffKey(tokenId));
    this.logger.debug({ tokenId }, "Guest handoff token invalidated");
  }

  // ============================================================================
  // Session Locking (Idempotency Protection)
  // ============================================================================

  /**
   * Atomically acquire a lock and mark the session as used for a specific operation.
   *
   * This prevents race conditions where the same session could be used
   * multiple times (e.g., double-clicking "Create Account").
   */
  async acquireAndMarkAsUsed(
    sessionToken: string,
    operation: NonNullable<SessionData["usedFor"]>
  ): Promise<{ success: true; session: GetStartedSession } | { success: false; reason: string }> {
    const lockKey = `${this.SESSION_LOCK_PREFIX}${sessionToken}`;

    const lockResult = await this.lockService.tryWithLock(
      lockKey,
      async () => {
        const sessionData = await this.cache.get<SessionData>(this.buildKey(sessionToken));

        if (!sessionData) {
          return { success: false as const, reason: "Session not found or expired" };
        }

        if (!sessionData.emailVerified) {
          return { success: false as const, reason: "Session email not verified" };
        }

        if (sessionData.usedAt) {
          this.logger.warn(
            { sessionId: sessionToken, usedFor: sessionData.usedFor, usedAt: sessionData.usedAt },
            "Session already used"
          );
          return {
            success: false as const,
            reason: `Session already used for ${sessionData.usedFor}`,
          };
        }

        // Mark session as used
        const updatedData = buildSessionData(
          {
            id: sessionData.id,
            email: sessionData.email,
            emailVerified: sessionData.emailVerified,
            createdAt: sessionData.createdAt,
          },
          {
            usedAt: new Date().toISOString(),
            usedFor: operation,
            accountStatus: sessionData.accountStatus,
            firstName: sessionData.firstName,
            lastName: sessionData.lastName,
            phone: sessionData.phone,
            address: sessionData.address,
            sfAccountId: sessionData.sfAccountId,
            whmcsClientId: sessionData.whmcsClientId,
            eligibilityStatus: sessionData.eligibilityStatus,
          }
        );

        const remainingTtl = this.calculateRemainingTtl(sessionData.createdAt);
        await this.cache.set(this.buildKey(sessionToken), updatedData, remainingTtl);

        this.logger.debug({ sessionId: sessionToken, operation }, "Session marked as used");

        return {
          success: true as const,
          session: {
            ...updatedData,
            expiresAt: this.calculateExpiresAt(sessionData.createdAt),
          },
        };
      },
      { ttlMs: this.sessionLockTtlMs, maxRetries: 0 }
    );

    if (!lockResult.success) {
      this.logger.warn(
        { sessionId: sessionToken },
        "Failed to acquire session lock - operation in progress"
      );
      return { success: false, reason: "Operation already in progress" };
    }

    return lockResult.result;
  }

  // ============================================================================
  // Private Helpers
  // ============================================================================

  private buildKey(sessionId: string): string {
    return `${this.SESSION_PREFIX}${sessionId}`;
  }

  private buildHandoffKey(tokenId: string): string {
    return `${this.HANDOFF_PREFIX}${tokenId}`;
  }

  private calculateExpiresAt(createdAt: string): string {
    const created = new Date(createdAt);
    const expires = new Date(created.getTime() + this.ttlSeconds * 1000);
    return expires.toISOString();
  }

  private calculateRemainingTtl(createdAt: string): number {
    const created = new Date(createdAt).getTime();
    const elapsed = (Date.now() - created) / 1000;
    return Math.max(1, Math.floor(this.ttlSeconds - elapsed));
  }
}
