import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { genericOAuth } from "better-auth/plugins";
import type { PrismaClient } from "@prisma/client";

/**
 * Required OIDC environment variables when OIDC is enabled
 */
const REQUIRED_OIDC_ENV_VARS = [
  "OIDC_ISSUER",
  "OIDC_CLIENT_ID",
  "OIDC_CLIENT_SECRET",
  "OIDC_REDIRECT_URI",
] as const;

/**
 * Resolve BetterAuth base URL from explicit auth URL or API URL.
 * BetterAuth uses this to generate absolute callback/error URLs.
 */
export function getBetterAuthBaseUrl(): string | undefined {
  const configured = process.env.BETTER_AUTH_URL ?? process.env.NEXT_PUBLIC_API_URL;

  if (!configured || configured.trim() === "") {
    if (process.env.NODE_ENV === "production") {
      throw new Error(
        "Missing BetterAuth base URL in production. Set BETTER_AUTH_URL (preferred) or NEXT_PUBLIC_API_URL."
      );
    }
    return undefined;
  }

  let parsed: URL;
  try {
    parsed = new URL(configured);
  } catch (urlError: unknown) {
    const detail = urlError instanceof Error ? urlError.message : String(urlError);
    throw new Error(
      `BetterAuth base URL must be a valid URL. Current value: "${configured}". Parse error: ${detail}.`
    );
  }

  if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
    throw new Error(
      `BetterAuth base URL must use https in production. Current value: "${configured}".`
    );
  }

  return parsed.origin;
}

/**
 * Check if OIDC authentication is enabled via environment variable
 */
export function isOidcEnabled(): boolean {
  const enabled = process.env.OIDC_ENABLED;
  return enabled === "true" || enabled === "1";
}

/**
 * Validates OIDC configuration at startup.
 * Throws an error if OIDC is enabled but required environment variables are missing.
 *
 * @throws Error if OIDC is enabled but required vars are missing or empty
 */
export function validateOidcConfig(): void {
  if (!isOidcEnabled()) {
    // OIDC is disabled, no validation needed
    return;
  }

  const missingVars: string[] = [];

  for (const envVar of REQUIRED_OIDC_ENV_VARS) {
    const value = process.env[envVar];
    if (!value || value.trim() === "") {
      missingVars.push(envVar);
    }
  }

  if (missingVars.length > 0) {
    throw new Error(
      `OIDC authentication is enabled (OIDC_ENABLED=true) but required environment variables are missing or empty: ${missingVars.join(", ")}. ` +
        `Either set these variables or disable OIDC by setting OIDC_ENABLED=false.`
    );
  }

  // Additional validation: OIDC_ISSUER should end with a trailing slash for proper discovery URL
  const issuer = process.env.OIDC_ISSUER;
  if (issuer && !issuer.endsWith("/")) {
    throw new Error(
      `OIDC_ISSUER must end with a trailing slash (/). Current value: "${issuer}". ` +
        `The discovery URL is constructed by appending ".well-known/openid-configuration" to the issuer.`
    );
  }

  // Additional validation: OIDC_REDIRECT_URI must be a valid URL with /auth/oauth2/callback path
  validateRedirectUri();
}

/**
 * Validates the OIDC_REDIRECT_URI environment variable.
 * - Must be a parseable URL
 * - Path must start with /auth/oauth2/callback
 * - Warns (but does not throw) if using localhost in production
 *
 * @throws Error if URL is invalid or path does not start with /auth/oauth2/callback
 */
function validateRedirectUri(): void {
  const redirectUri = process.env.OIDC_REDIRECT_URI;
  if (!redirectUri || redirectUri.trim() === "") {
    // Already caught by REQUIRED_OIDC_ENV_VARS check above
    return;
  }

  let parsed: URL;
  try {
    parsed = new URL(redirectUri);
  } catch (urlError: unknown) {
    const detail = urlError instanceof Error ? urlError.message : String(urlError);
    throw new Error(
      `OIDC_REDIRECT_URI must be a valid URL. Current value: "${redirectUri}". ` +
        `Parse error: ${detail}. ` +
        `Example: "https://api.example.com/auth/oauth2/callback/authentik".`
    );
  }

  if (!parsed.pathname.startsWith("/auth/oauth2/callback")) {
    throw new Error(
      `OIDC_REDIRECT_URI path must start with "/auth/oauth2/callback". Current path: "${parsed.pathname}". ` +
        `Example: "https://api.example.com/auth/oauth2/callback/authentik".`
    );
  }

  if (
    process.env.NODE_ENV === "production" &&
    (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1")
  ) {
    console.warn(
      `[AUTH WARNING] OIDC_REDIRECT_URI uses localhost ("${redirectUri}") in production. ` +
        `This is likely a misconfiguration. Use a public domain for production deployments.`
    );
  }
}

/**
 * Get OIDC plugins configuration.
 * Returns empty array if OIDC is disabled, otherwise returns configured OAuth plugin.
 */
function getOidcPlugins(): ReturnType<typeof genericOAuth>[] {
  if (!isOidcEnabled()) {
    return [];
  }

  const clientId = process.env.OIDC_CLIENT_ID;
  const clientSecret = process.env.OIDC_CLIENT_SECRET;
  const issuer = process.env.OIDC_ISSUER;
  const redirectUri = process.env.OIDC_REDIRECT_URI;

  if (!clientId) {
    throw new Error("OIDC_CLIENT_ID is required when OIDC is enabled but was not set.");
  }
  if (!clientSecret) {
    throw new Error("OIDC_CLIENT_SECRET is required when OIDC is enabled but was not set.");
  }
  if (!issuer) {
    throw new Error("OIDC_ISSUER is required when OIDC is enabled but was not set.");
  }
  if (!redirectUri) {
    throw new Error("OIDC_REDIRECT_URI is required when OIDC is enabled but was not set.");
  }

  return [
    genericOAuth({
      config: [
        {
          providerId: "authentik",
          clientId,
          clientSecret,
          discoveryUrl: `${issuer}.well-known/openid-configuration`,
          redirectURI: redirectUri,
          pkce: true,
          scopes: ["openid", "profile", "email"],
        },
      ],
    }),
  ];
}

/**
 * Build the list of trusted origins from environment variables.
 *
 * Sources (in order):
 *  - NEXT_PUBLIC_APP_URL  — primary frontend URL
 *  - NEXT_PUBLIC_API_URL  — API's own origin
 *  - TRUSTED_ORIGINS      — comma-separated additional origins
 *  - localhost fallbacks   — only when NODE_ENV !== "production"
 *
 * The returned list is deduplicated and empty strings are filtered out.
 */
export function getTrustedOrigins(): string[] {
  const origins: string[] = [];

  // Environment-driven origins
  if (process.env.NEXT_PUBLIC_APP_URL) {
    origins.push(process.env.NEXT_PUBLIC_APP_URL);
  }

  if (process.env.NEXT_PUBLIC_API_URL) {
    origins.push(process.env.NEXT_PUBLIC_API_URL);
  }

  // Comma-separated additional origins (validated)
  if (process.env.TRUSTED_ORIGINS) {
    const rawOrigins = process.env.TRUSTED_ORIGINS.split(",")
      .map((o) => o.trim())
      .filter((o) => o !== "");
    for (const origin of rawOrigins) {
      try {
        const parsed = new URL(origin);
        if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
          console.warn(`[AUTH] Ignoring non-HTTP origin in TRUSTED_ORIGINS: "${origin}"`);
          continue;
        }
        origins.push(origin);
      } catch (urlError: unknown) {
        const detail = urlError instanceof Error ? urlError.message : String(urlError);
        console.warn(`[AUTH] Ignoring invalid URL in TRUSTED_ORIGINS: "${origin}" (${detail})`);
      }
    }
  }

  // Localhost fallbacks for development only
  if (process.env.NODE_ENV !== "production") {
    origins.push("http://localhost:3000", "http://localhost:3001");
  }

  // Deduplicate and filter empty strings
  return [...new Set(origins)].filter((o) => o !== "");
}

export function createAuth(prisma: PrismaClient) {
  // Validate OIDC configuration at startup - fail fast if misconfigured
  validateOidcConfig();

  const baseURL = getBetterAuthBaseUrl();

  return betterAuth({
    baseURL,
    basePath: "/auth",
    database: prismaAdapter(prisma, {
      provider: "postgresql",
    }),
    emailAndPassword: {
      enabled: true,
    },
    plugins: [...getOidcPlugins()],
    logger: {
      disabled: false,
      level: "error",
    },
    session: {
      expiresIn: 60 * 60 * 24 * 7, // 7 days absolute max
      updateAge: 60 * 60 * 2, // 2 hours — minimum session age before BetterAuth refreshes the expiry on next request
    },
    advanced: {
      database: {
        // BetterAuth's default ID generator emits opaque strings; our auth tables use UUID PKs.
        generateId: "uuid",
      },
      defaultCookieAttributes: {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production",
        sameSite: "lax" as const,
        ...(process.env.COOKIE_DOMAIN ? { domain: process.env.COOKIE_DOMAIN } : {}),
      },
    },
    trustedOrigins: getTrustedOrigins(),
  });
}

export type Auth = ReturnType<typeof createAuth>;
