import { Injectable, Logger } from "@nestjs/common";
import type { PrismaClient } from "@prisma/client";
import type { IncomingMessage, ServerResponse } from "http";
import { toNodeHandler } from "better-auth/node";
import type { AuthConfigResponse, AuthProviderConfig } from "@mosaic/shared";
import { PrismaService } from "../prisma/prisma.service";
import { createAuth, isOidcEnabled, type Auth } from "./auth.config";

/** Duration in milliseconds to cache the OIDC health check result */
const OIDC_HEALTH_CACHE_TTL_MS = 30_000;

/** Timeout in milliseconds for the OIDC discovery URL fetch */
const OIDC_HEALTH_TIMEOUT_MS = 2_000;

/** Number of consecutive health-check failures before escalating to error level */
const HEALTH_ESCALATION_THRESHOLD = 3;

/** Verified session shape returned by BetterAuth's getSession */
interface VerifiedSession {
  user: Record<string, unknown>;
  session: Record<string, unknown>;
}

interface SessionHeaderCandidate {
  headers: Record<string, string>;
}

@Injectable()
export class AuthService {
  private readonly logger = new Logger(AuthService.name);
  private readonly auth: Auth;
  private readonly nodeHandler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;

  /** Timestamp of the last OIDC health check */
  private lastHealthCheck = 0;
  /** Cached result of the last OIDC health check */
  private lastHealthResult = false;
  /** Consecutive OIDC health check failure count for log-level escalation */
  private consecutiveHealthFailures = 0;

  constructor(private readonly prisma: PrismaService) {
    // PrismaService extends PrismaClient and is compatible with BetterAuth's adapter
    // Cast is safe as PrismaService provides all required PrismaClient methods
    // TODO(#411): BetterAuth returns opaque types — replace when upstream exports typed interfaces
    this.auth = createAuth(this.prisma as unknown as PrismaClient);
    this.nodeHandler = toNodeHandler(this.auth);
  }

  /**
   * Get BetterAuth instance
   */
  getAuth(): Auth {
    return this.auth;
  }

  /**
   * Get Node.js-compatible request handler for BetterAuth.
   * Wraps BetterAuth's Web API handler to work with Express/Node.js req/res.
   */
  getNodeHandler(): (req: IncomingMessage, res: ServerResponse) => Promise<void> {
    return this.nodeHandler;
  }

  /**
   * Get user by ID
   */
  async getUserById(userId: string): Promise<{
    id: string;
    email: string;
    name: string;
    authProviderId: string | null;
  } | null> {
    return this.prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        email: true,
        name: true,
        authProviderId: true,
      },
    });
  }

  /**
   * Get user by email
   */
  async getUserByEmail(email: string): Promise<{
    id: string;
    email: string;
    name: string;
    authProviderId: string | null;
  } | null> {
    return this.prisma.user.findUnique({
      where: { email },
      select: {
        id: true,
        email: true,
        name: true,
        authProviderId: true,
      },
    });
  }

  /**
   * Verify session token
   * Returns session data if valid, null if invalid or expired.
   * Only known-safe auth errors return null; everything else propagates as 500.
   */
  async verifySession(token: string): Promise<VerifiedSession | null> {
    let sawNonError = false;

    for (const candidate of this.buildSessionHeaderCandidates(token)) {
      try {
        // TODO(#411): BetterAuth getSession returns opaque types — replace when upstream exports typed interfaces
        const session = await this.auth.api.getSession(candidate);

        if (!session) {
          continue;
        }

        return {
          user: session.user as Record<string, unknown>,
          session: session.session as Record<string, unknown>,
        };
      } catch (error: unknown) {
        if (error instanceof Error) {
          if (this.isExpectedAuthError(error.message)) {
            continue;
          }

          // Infrastructure or unexpected — propagate as 500
          const safeMessage = (error.stack ?? error.message).replace(
            /Bearer\s+\S+/gi,
            "Bearer [REDACTED]"
          );
          this.logger.error("Session verification failed due to unexpected error", safeMessage);
          throw error;
        }

        // Non-Error thrown values — log once for observability, treat as auth failure
        if (!sawNonError) {
          const errorDetail = typeof error === "string" ? error : JSON.stringify(error);
          this.logger.warn("Session verification received non-Error thrown value", errorDetail);
          sawNonError = true;
        }
      }
    }

    return null;
  }

  private buildSessionHeaderCandidates(token: string): SessionHeaderCandidate[] {
    return [
      {
        headers: {
          cookie: `__Secure-better-auth.session_token=${token}`,
        },
      },
      {
        headers: {
          cookie: `better-auth.session_token=${token}`,
        },
      },
      {
        headers: {
          cookie: `__Host-better-auth.session_token=${token}`,
        },
      },
      {
        headers: {
          authorization: `Bearer ${token}`,
        },
      },
    ];
  }

  private isExpectedAuthError(message: string): boolean {
    const normalized = message.toLowerCase();
    return (
      normalized.includes("invalid token") ||
      normalized.includes("token expired") ||
      normalized.includes("session expired") ||
      normalized.includes("session not found") ||
      normalized.includes("invalid session") ||
      normalized === "unauthorized" ||
      normalized === "expired"
    );
  }

  /**
   * Check if the OIDC provider (Authentik) is reachable by fetching the discovery URL.
   * Results are cached for 30 seconds to prevent repeated network calls.
   *
   * @returns true if the provider responds with an HTTP 2xx status, false otherwise
   */
  async isOidcProviderReachable(): Promise<boolean> {
    const now = Date.now();

    // Return cached result if still valid
    if (now - this.lastHealthCheck < OIDC_HEALTH_CACHE_TTL_MS) {
      this.logger.debug("OIDC health check: returning cached result");
      return this.lastHealthResult;
    }

    const discoveryUrl = `${process.env.OIDC_ISSUER ?? ""}.well-known/openid-configuration`;
    this.logger.debug(`OIDC health check: fetching ${discoveryUrl}`);

    try {
      const response = await fetch(discoveryUrl, {
        signal: AbortSignal.timeout(OIDC_HEALTH_TIMEOUT_MS),
      });

      this.lastHealthCheck = Date.now();
      this.lastHealthResult = response.ok;

      if (response.ok) {
        if (this.consecutiveHealthFailures > 0) {
          this.logger.log(
            `OIDC provider recovered after ${String(this.consecutiveHealthFailures)} consecutive failure(s)`
          );
        }
        this.consecutiveHealthFailures = 0;
      } else {
        this.consecutiveHealthFailures++;
        const logLevel =
          this.consecutiveHealthFailures >= HEALTH_ESCALATION_THRESHOLD ? "error" : "warn";
        this.logger[logLevel](
          `OIDC provider returned non-OK status: ${String(response.status)} from ${discoveryUrl}`
        );
      }

      return this.lastHealthResult;
    } catch (error: unknown) {
      this.lastHealthCheck = Date.now();
      this.lastHealthResult = false;
      this.consecutiveHealthFailures++;

      const message = error instanceof Error ? error.message : String(error);
      const logLevel =
        this.consecutiveHealthFailures >= HEALTH_ESCALATION_THRESHOLD ? "error" : "warn";
      this.logger[logLevel](`OIDC provider unreachable at ${discoveryUrl}: ${message}`);

      return false;
    }
  }

  /**
   * Get authentication configuration for the frontend.
   * Returns available auth providers so the UI can render login options dynamically.
   * When OIDC is enabled, performs a health check to verify the provider is reachable.
   */
  async getAuthConfig(): Promise<AuthConfigResponse> {
    const providers: AuthProviderConfig[] = [{ id: "email", name: "Email", type: "credentials" }];

    if (isOidcEnabled() && (await this.isOidcProviderReachable())) {
      providers.push({ id: "authentik", name: "Authentik", type: "oauth" });
    }

    return { providers };
  }
}
