import type {
  AuthenticatedRequest,
  OIDCProviderConfig,
} from "../../../types/index.js";
import type { Router } from "express";
import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import type { SSOProviderType } from "../../../types/index.js";
import { createCurrentSsoProviderRepository } from "../repositories/factory.js";
import {
  getOIDCConfigFromEnv,
  isOIDCEnvOverrideEnabled,
} from "./user-oidc-utils.js";
import {
  decryptSsoConfigSecrets,
  encryptSsoConfigSecrets,
} from "../../utils/system-secret-crypto.js";
import { isTrustedProxyAuthEnabled } from "../../utils/trusted-proxy-auth.js";

function isOidcLike(type: SSOProviderType): boolean {
  return type === "oidc" || type === "github" || type === "google";
}

export function isValidOidcIssuer(value: unknown): boolean {
  if (typeof value !== "string") return false;
  try {
    const url = new URL(value);
    return (
      ["http:", "https:"].includes(url.protocol) &&
      !/\/userinfo\/?$/i.test(url.pathname)
    );
  } catch {
    return false;
  }
}

const authManager = AuthManager.getInstance();

/**
 * SSO secrets belong to the installation, not to a user: `sso_providers` has no
 * userId and the values must be readable during login, before anyone is
 * authenticated. They are encrypted with the system key rather than a user DEK.
 * Values written by the previous base64 scheme still decode, and are upgraded
 * the next time the provider is saved.
 */
async function decryptProviderConfig(
  configJson: string,
  _userId: string,
): Promise<Record<string, unknown>> {
  let config: Record<string, unknown>;
  try {
    config = JSON.parse(configJson);
  } catch {
    return {};
  }
  return decryptSsoConfigSecrets(config);
}

async function encryptProviderConfig(
  config: Record<string, unknown>,
  _userId: string,
  _providerId: string,
): Promise<string> {
  return JSON.stringify(await encryptSsoConfigSecrets(config));
}

function applyProviderDefaults(
  type: SSOProviderType,
  config: Partial<OIDCProviderConfig>,
): Partial<OIDCProviderConfig> {
  if (type === "github") {
    return {
      authorization_url: "https://github.com/login/oauth/authorize",
      token_url: "https://github.com/login/oauth/access_token",
      issuer_url: "https://github.com",
      identifier_path: "id",
      name_path: "name",
      scopes: "read:user user:email",
      userinfo_url: "https://api.github.com/user",
      ...config,
    };
  }
  if (type === "google") {
    return {
      authorization_url: "https://accounts.google.com/o/oauth2/v2/auth",
      token_url: "https://oauth2.googleapis.com/token",
      issuer_url: "https://accounts.google.com",
      identifier_path: "sub",
      name_path: "name",
      scopes: "openid email profile",
      ...config,
    };
  }
  return config;
}

export function registerSSOProviderRoutes(router: Router): void {
  const requireAdmin = authManager.createAdminMiddleware();

  /**
   * @openapi
   * /users/sso-providers:
   *   get:
   *     summary: List enabled SSO providers (public)
   *     description: Returns public info for all enabled SSO providers for the login page.
   *     tags:
   *       - SSO
   *     responses:
   *       200:
   *         description: Array of public SSO provider objects.
   */
  router.get("/sso-providers", async (_req, res) => {
    try {
      const envConfig = getOIDCConfigFromEnv();
      if (envConfig && isOIDCEnvOverrideEnabled()) {
        return res.json([
          { id: 0, name: "SSO", type: "oidc", displayOrder: 0 },
        ]);
      }

      const providers =
        await createCurrentSsoProviderRepository().listEnabledPublic();

      // If no DB providers exist, synthesize one from env vars so SSO login
      // remains available when configured purely via environment variables.
      if (providers.length === 0) {
        if (envConfig) {
          providers.push({ id: 0, name: "SSO", type: "oidc", displayOrder: 0 });
        }
      }

      res.json(providers);
    } catch (err) {
      authLogger.error("Failed to list SSO providers", err);
      res.status(500).json({ error: "Failed to list SSO providers" });
    }
  });

  /**
   * @openapi
   * /users/sso-providers/admin:
   *   get:
   *     summary: List all SSO providers (admin)
   *     description: Returns full SSO provider list with decrypted configs for the admin panel.
   *     tags:
   *       - SSO
   *     responses:
   *       200:
   *         description: Array of full SSO provider objects with decrypted config.
   */
  router.get("/sso-providers/admin", requireAdmin, async (req, res) => {
    const userId = (req as AuthenticatedRequest).userId;
    try {
      const rows = await createCurrentSsoProviderRepository().listAll();

      const result = await Promise.all(
        rows.map(async (row) => ({
          ...row,
          config: await decryptProviderConfig(row.config, userId),
        })),
      );
      res.json(result);
    } catch (err) {
      authLogger.error("Failed to list SSO providers (admin)", err);
      res.status(500).json({ error: "Failed to list SSO providers" });
    }
  });

  /**
   * @openapi
   * /users/sso-providers:
   *   post:
   *     summary: Create SSO provider
   *     description: Creates a new SSO provider configuration.
   *     tags:
   *       - SSO
   *     requestBody:
   *       required: true
   *       content:
   *         application/json:
   *           schema:
   *             type: object
   *     responses:
   *       201:
   *         description: Provider created.
   *       400:
   *         description: Validation error.
   */
  router.post("/sso-providers", requireAdmin, async (req, res) => {
    const userId = (req as AuthenticatedRequest).userId;
    try {
      const {
        name,
        type,
        enabled = true,
        displayOrder = 0,
        config: rawConfig = {},
      } = req.body as {
        name: string;
        type: SSOProviderType;
        enabled?: boolean;
        displayOrder?: number;
        config?: Record<string, unknown>;
      };

      if (!name || typeof name !== "string" || !name.trim()) {
        return res.status(400).json({ error: "Provider name is required" });
      }
      const validTypes: SSOProviderType[] = [
        "oidc",
        "ldap",
        "github",
        "google",
      ];
      if (!validTypes.includes(type)) {
        return res.status(400).json({ error: "Invalid provider type" });
      }
      if (isTrustedProxyAuthEnabled() && enabled && isOidcLike(type)) {
        return res.status(409).json({
          error:
            "OIDC providers cannot be enabled with trusted proxy authentication",
        });
      }

      const configWithDefaults =
        type === "github" || type === "google"
          ? applyProviderDefaults(
              type,
              rawConfig as Partial<OIDCProviderConfig>,
            )
          : rawConfig;

      if (type === "oidc" || type === "github" || type === "google") {
        const c = configWithDefaults as Partial<OIDCProviderConfig>;
        const missing = [
          "client_id",
          "client_secret",
          "issuer_url",
          "authorization_url",
          "token_url",
        ].filter((f) => !c[f as keyof OIDCProviderConfig]);
        if (missing.length > 0 && type === "oidc") {
          return res.status(400).json({
            error: `Missing required OIDC fields: ${missing.join(", ")}`,
          });
        }
        if (c.issuer_url && !isValidOidcIssuer(c.issuer_url)) {
          return res.status(400).json({
            error:
              "Issuer URL must be an HTTP(S) issuer and not a userinfo endpoint",
          });
        }
        if (
          (type === "github" || type === "google") &&
          (!c.client_id || !c.client_secret)
        ) {
          return res
            .status(400)
            .json({ error: "Client ID and Client Secret are required" });
        }
      }

      if (type === "ldap") {
        const c = configWithDefaults as Record<string, unknown>;
        const missing = [
          "host",
          "port",
          "bindDN",
          "bindPassword",
          "userSearchBase",
          "userSearchFilter",
          "usernameAttribute",
        ].filter((f) => !c[f]);
        if (missing.length > 0) {
          return res.status(400).json({
            error: `Missing required LDAP fields: ${missing.join(", ")}`,
          });
        }
      }

      const tempId = `new-${Date.now()}`;
      const encryptedConfig = await encryptProviderConfig(
        configWithDefaults as Record<string, unknown>,
        userId,
        tempId,
      );

      const inserted = await createCurrentSsoProviderRepository().create({
        name: name.trim(),
        type,
        enabled,
        displayOrder,
        config: encryptedConfig,
      });

      authLogger.info("SSO provider created", {
        operation: "sso_provider_create",
        userId,
        type,
        providerId: inserted.id,
      });
      res.status(201).json({
        ...inserted,
        config: await decryptProviderConfig(inserted.config, userId),
      });
    } catch (err) {
      authLogger.error("Failed to create SSO provider", err);
      res.status(500).json({ error: "Failed to create SSO provider" });
    }
  });

  /**
   * @openapi
   * /users/sso-providers/{id}:
   *   put:
   *     summary: Update SSO provider
   *     description: Updates an existing SSO provider configuration.
   *     tags:
   *       - SSO
   *     parameters:
   *       - in: path
   *         name: id
   *         required: true
   *         schema:
   *           type: integer
   *     responses:
   *       200:
   *         description: Provider updated.
   *       404:
   *         description: Provider not found.
   */
  router.put("/sso-providers/:id", requireAdmin, async (req, res) => {
    const userId = (req as AuthenticatedRequest).userId;
    const providerId = parseInt(req.params.id as string, 10);
    if (isNaN(providerId)) {
      return res.status(400).json({ error: "Invalid provider ID" });
    }
    try {
      const providerRepository = createCurrentSsoProviderRepository();
      const existing = await providerRepository.findById(providerId);
      if (!existing) {
        return res.status(404).json({ error: "SSO provider not found" });
      }

      const {
        name,
        type,
        enabled,
        displayOrder,
        config: rawConfig,
      } = req.body as {
        name?: string;
        type?: SSOProviderType;
        enabled?: boolean;
        displayOrder?: number;
        config?: Record<string, unknown>;
      };

      const effectiveType = type ?? (existing.type as SSOProviderType);
      const effectiveEnabled = enabled ?? existing.enabled;
      if (
        isTrustedProxyAuthEnabled() &&
        effectiveEnabled &&
        isOidcLike(effectiveType)
      ) {
        return res.status(409).json({
          error:
            "OIDC providers cannot be enabled with trusted proxy authentication",
        });
      }

      let encryptedConfig = existing.config;
      if (rawConfig !== undefined) {
        const existingDecrypted = await decryptProviderConfig(
          existing.config,
          userId,
        );
        const mergedConfig = {
          ...JSON.parse(
            existingDecrypted ? JSON.stringify(existingDecrypted) : "{}",
          ),
          ...rawConfig,
        };
        if (
          isOidcLike(effectiveType) &&
          mergedConfig.issuer_url &&
          !isValidOidcIssuer(mergedConfig.issuer_url)
        ) {
          return res.status(400).json({
            error:
              "Issuer URL must be an HTTP(S) issuer and not a userinfo endpoint",
          });
        }
        encryptedConfig = await encryptProviderConfig(
          mergedConfig,
          userId,
          String(providerId),
        );
      }

      const updated = await providerRepository.update(providerId, {
        ...(name !== undefined ? { name: name.trim() } : {}),
        ...(type !== undefined ? { type } : {}),
        ...(enabled !== undefined ? { enabled } : {}),
        ...(displayOrder !== undefined ? { displayOrder } : {}),
        config: encryptedConfig,
        updatedAt: new Date().toISOString(),
      });

      if (!updated) {
        return res.status(404).json({ error: "SSO provider not found" });
      }

      authLogger.info("SSO provider updated", {
        operation: "sso_provider_update",
        userId,
        providerId,
      });
      res.json({
        ...updated,
        config: await decryptProviderConfig(updated.config, userId),
      });
    } catch (err) {
      authLogger.error("Failed to update SSO provider", err);
      res.status(500).json({ error: "Failed to update SSO provider" });
    }
  });

  /**
   * @openapi
   * /users/sso-providers/{id}:
   *   delete:
   *     summary: Delete SSO provider
   *     description: Deletes an SSO provider. Blocked if users are associated.
   *     tags:
   *       - SSO
   *     parameters:
   *       - in: path
   *         name: id
   *         required: true
   *         schema:
   *           type: integer
   *     responses:
   *       200:
   *         description: Provider deleted.
   *       409:
   *         description: Users are associated with this provider.
   *       404:
   *         description: Provider not found.
   */
  router.delete("/sso-providers/:id", requireAdmin, async (req, res) => {
    const userId = (req as AuthenticatedRequest).userId;
    const providerId = parseInt(req.params.id as string, 10);
    if (isNaN(providerId)) {
      return res.status(400).json({ error: "Invalid provider ID" });
    }
    try {
      const providerRepository = createCurrentSsoProviderRepository();
      const existing = await providerRepository.findById(providerId);
      if (!existing) {
        return res.status(404).json({ error: "SSO provider not found" });
      }

      const associatedUserCount =
        await providerRepository.countUsersByProviderId(providerId);
      if (associatedUserCount > 0) {
        return res.status(409).json({
          error: `Cannot delete provider: ${associatedUserCount} user(s) are associated with it`,
        });
      }

      await providerRepository.delete(providerId);
      authLogger.info("SSO provider deleted", {
        operation: "sso_provider_delete",
        userId,
        providerId,
      });
      res.json({ message: "SSO provider deleted" });
    } catch (err) {
      authLogger.error("Failed to delete SSO provider", err);
      res.status(500).json({ error: "Failed to delete SSO provider" });
    }
  });
}

export { decryptProviderConfig, encryptProviderConfig };
