import type {
  DirectorySyncEffect,
  DirectorySyncStatus,
  OrgSsoStatus,
  PluginDatabaseConfig,
  SsoBeginError,
  SsoCompleteError,
  SsoController,
  SsoDecisionError,
  SsoFlow,
  SsoMutationError,
  SsoPlugin,
  SsoPortalError,
  SsoProfile,
  SsoResolutionDecision,
  SsoRouteDecision,
  SsoValidateError,
  SsoWebhookError,
  SsoWebhookEvent,
} from "@trigger.dev/plugins";
import type { PrismaClient } from "@trigger.dev/database";
import { ResultAsync } from "neverthrow";
import { SsoFallback } from "./fallback.js";
export type { SsoController } from "@trigger.dev/plugins";

export type SsoPrismaInput = PrismaClient | { primary: PrismaClient; replica: PrismaClient };

export type SsoCreateOptions = {
  // When true, skip loading the plugin. Useful for tests and for
  // contributors who don't have the cloud plugin installed.
  forceFallback?: boolean;
  // Override the dynamic importer. Lets tests inject a fake plugin
  // module or a synthetic ERR_MODULE_NOT_FOUND failure without touching
  // the real plugin install on disk.
  importer?: (moduleName: string) => Promise<{ default: SsoPlugin }>;
  // Writer/reader connection URLs + pool sizes for a plugin that owns its
  // own database client, resolved by the host from its env so the plugin
  // follows the host's writer/replica topology. The fallback ignores this —
  // it queries through the Prisma clients passed as `SsoPrismaInput`.
  database?: PluginDatabaseConfig;
};

// Loads the cloud plugin lazily; falls back to the OSS no-op
// implementation if not installed. Synchronous create() avoids
// top-level await (not supported in the webapp's CJS build).
export class LazyController implements SsoController {
  private readonly _init: Promise<SsoController>;

  constructor(prisma: SsoPrismaInput, options?: SsoCreateOptions) {
    this._init = this.load(prisma, options);
  }

  private async load(prisma: SsoPrismaInput, options?: SsoCreateOptions): Promise<SsoController> {
    if (options?.forceFallback) {
      return new SsoFallback(prisma).create();
    }
    const moduleName = "@triggerdotdev/plugins/sso";
    const importer =
      options?.importer ?? ((m: string) => import(m) as Promise<{ default: SsoPlugin }>);
    try {
      const module = await importer(moduleName);
      const plugin: SsoPlugin = module.default;
      console.log("SSO: using plugin implementation");
      return plugin.create({ database: options?.database });
    } catch (err) {
      // Distinguish the two failure modes the dynamic import can hit:
      //
      // 1. The plugin itself is absent (no install) — expected on OSS
      //    deployments. Quiet by default; logged when SSO_LOG_FALLBACK=1
      //    so contributors can opt into a visible signal locally.
      // 2. The plugin module loaded but its initialization failed
      //    (transitive dep missing, syntax error, …). Always logged
      //    loudly because this indicates a real bug.
      //
      // Node throws ERR_MODULE_NOT_FOUND for both cases, so we
      // disambiguate by checking whether the missing specifier is the
      // plugin's own module name.
      const code = (err as NodeJS.ErrnoException | undefined)?.code;
      const message = err instanceof Error ? err.message : String(err);
      const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
      const isPluginItselfMissing = isModuleNotFound && message.includes(moduleName);

      if (!isPluginItselfMissing) {
        console.error(
          "SSO: plugin found but failed to load; falling back to default implementation",
          err
        );
      } else if (process.env.SSO_LOG_FALLBACK === "1" || process.env.SSO_LOG_FALLBACK === "true") {
        console.log("SSO: no plugin installed (ERR_MODULE_NOT_FOUND); using fallback");
      }
      return new SsoFallback(prisma).create();
    }
  }

  private c(): Promise<SsoController> {
    return this._init;
  }

  // Bridges a Promise<ResultAsync<T,E>> back into a ResultAsync<T,E>.
  // The `load()` method above always resolves (it catches and falls
  // back), so `this.c()` is safe to lift via fromSafePromise. Inner
  // controller methods are expected to never throw — they return
  // errors via the Result instead — so the .andThen flatten is total.
  private call<T, E>(factory: (c: SsoController) => ResultAsync<T, E>): ResultAsync<T, E> {
    return ResultAsync.fromSafePromise(this.c().then(factory)).andThen((r) => r);
  }

  async isUsingPlugin(): Promise<boolean> {
    return (await this.c()).isUsingPlugin();
  }

  getStatus(organizationId: string): ResultAsync<OrgSsoStatus, SsoDecisionError> {
    return this.call((c) => c.getStatus(organizationId));
  }

  generatePortalLink(params: {
    organizationId: string;
    userId: string;
    intent: "sso" | "domain_verification" | "dsync";
    returnUrl: string;
  }): ResultAsync<{ url: string }, SsoPortalError> {
    return this.call((c) => c.generatePortalLink(params));
  }

  setEnforced(params: {
    organizationId: string;
    enforced: boolean;
  }): ResultAsync<void, SsoMutationError> {
    return this.call((c) => c.setEnforced(params));
  }

  setJitProvisioningEnabled(params: {
    organizationId: string;
    enabled: boolean;
  }): ResultAsync<void, SsoMutationError> {
    return this.call((c) => c.setJitProvisioningEnabled(params));
  }

  setJitDefaultRole(params: {
    organizationId: string;
    roleId: string | null;
  }): ResultAsync<void, SsoMutationError> {
    return this.call((c) => c.setJitDefaultRole(params));
  }

  updateConfig(params: {
    organizationId: string;
    enforced: boolean;
    jitProvisioningEnabled: boolean;
    jitDefaultRoleId: string | null;
  }): ResultAsync<void, SsoMutationError> {
    return this.call((c) => c.updateConfig(params));
  }

  getDirectorySyncStatus(
    organizationId: string
  ): ResultAsync<DirectorySyncStatus, SsoDecisionError> {
    return this.call((c) => c.getDirectorySyncStatus(organizationId));
  }

  setDirectoryGroupRole(params: {
    organizationId: string;
    groupId: string;
    roleId: string | null;
  }): ResultAsync<{ effects: DirectorySyncEffect[] }, SsoMutationError> {
    return this.call((c) => c.setDirectoryGroupRole(params));
  }

  setDirectoryDefaultRole(params: {
    organizationId: string;
    roleId: string | null;
  }): ResultAsync<void, SsoMutationError> {
    return this.call((c) => c.setDirectoryDefaultRole(params));
  }

  setAllowExternalDomainSync(params: {
    organizationId: string;
    allowed: boolean;
  }): ResultAsync<void, SsoMutationError> {
    return this.call((c) => c.setAllowExternalDomainSync(params));
  }

  getMembershipPolicy(
    organizationId: string
  ): ResultAsync<{ manualMembershipAllowed: boolean }, SsoDecisionError> {
    return this.call((c) => c.getMembershipPolicy(organizationId));
  }

  setAllowManualMembership(params: {
    organizationId: string;
    allowed: boolean;
  }): ResultAsync<void, SsoMutationError> {
    return this.call((c) => c.setAllowManualMembership(params));
  }

  recordMembershipRemoval(params: {
    organizationId: string;
    userId: string;
    reason: "manual_removal" | "self_leave";
  }): ResultAsync<void, SsoMutationError> {
    return this.call((c) => c.recordMembershipRemoval(params));
  }

  clearMembershipRemoval(params: {
    organizationId: string;
    userId: string;
  }): ResultAsync<void, SsoMutationError> {
    return this.call((c) => c.clearMembershipRemoval(params));
  }

  decideRouteForEmail(email: string): ResultAsync<SsoRouteDecision, SsoDecisionError> {
    return this.call((c) => c.decideRouteForEmail(email));
  }

  beginAuthorization(params: {
    email: string;
    redirectTo: string;
    flow: SsoFlow;
  }): ResultAsync<{ url: string }, SsoBeginError> {
    return this.call((c) => c.beginAuthorization(params));
  }

  completeAuthorization(params: {
    code: string;
    state: string;
  }): ResultAsync<{ profile: SsoProfile; redirectTo: string; flow: SsoFlow }, SsoCompleteError> {
    return this.call((c) => c.completeAuthorization(params));
  }

  completeIdpInitiatedAuthorization(params: {
    code: string;
  }): ResultAsync<{ profile: SsoProfile; redirectTo: string }, SsoCompleteError> {
    return this.call((c) => c.completeIdpInitiatedAuthorization(params));
  }

  validateSession(params: {
    userId: string;
    idpOrgId: string;
    connectionId: string;
  }): ResultAsync<{ valid: boolean }, SsoValidateError> {
    return this.call((c) => c.validateSession(params));
  }

  resolveSsoIdentity(params: {
    profile: SsoProfile;
  }): ResultAsync<SsoResolutionDecision, SsoMutationError> {
    return this.call((c) => c.resolveSsoIdentity(params));
  }

  attachSsoIdentity(params: {
    userId: string;
    profile: SsoProfile;
  }): ResultAsync<void, SsoMutationError> {
    return this.call((c) => c.attachSsoIdentity(params));
  }

  evaluateJit(params: {
    userId: string;
    idpOrgId: string;
  }): ResultAsync<
    { shouldProvision: boolean; organizationId: string; roleId: string | null },
    SsoMutationError
  > {
    return this.call((c) => c.evaluateJit(params));
  }

  verifyWebhook(params: {
    rawBody: string;
    headers: Record<string, string>;
  }): ResultAsync<{ event: SsoWebhookEvent }, SsoWebhookError> {
    return this.call((c) => c.verifyWebhook(params));
  }

  processWebhookEvent(
    event: SsoWebhookEvent
  ): ResultAsync<{ effects: DirectorySyncEffect[] }, SsoWebhookError> {
    return this.call((c) => c.processWebhookEvent(event));
  }
}

class Sso {
  // Synchronous — returns a lazy controller that resolves any installed
  // plugin on first call.
  create(prisma: SsoPrismaInput, options?: SsoCreateOptions): SsoController {
    return new LazyController(prisma, options);
  }
}

const loader = new Sso();

export default loader;
