/**
 * OIDC Provider configuration for Ghost Guild.
 *
 * ghostguild.org acts as the identity provider. Outline wiki is the sole
 * relying party (client). Members authenticate via the existing magic-link
 * flow, and the provider issues standard OIDC tokens so Outline can identify
 * them.
 */
import Provider from "oidc-provider";
import { MongoAdapter } from "./oidc-mongodb-adapter.js";
import Member from "../models/member.js";
import { connectDB } from "./mongoose.js";

if (process.env.NODE_ENV === 'production' && !process.env.OIDC_COOKIE_SECRET) {
  throw new Error('OIDC_COOKIE_SECRET must be set in production')
}

let _provider: InstanceType<typeof Provider> | null = null;

export async function getOidcProvider() {
  if (_provider) return _provider;

  const config = useRuntimeConfig();
  const issuer = process.env.OIDC_ISSUER || "https://ghostguild.org";

  _provider = new Provider(issuer, {
    adapter: MongoAdapter,

    clients: [
      {
        client_id: process.env.OIDC_CLIENT_ID || "outline-wiki",
        client_secret: process.env.OIDC_CLIENT_SECRET || "",
        redirect_uris: [
          "https://wiki.ghostguild.org/auth/oidc.callback",
          // Local development callback
          "http://localhost:3100/auth/oidc.callback",
        ],
        post_logout_redirect_uris: [
          "https://wiki.ghostguild.org",
          "http://localhost:3100",
        ],
        grant_types: ["authorization_code", "refresh_token"],
        response_types: ["code"],
        token_endpoint_auth_method: "client_secret_post",
      },
    ],

    claims: {
      openid: ["sub"],
      profile: ["name", "preferred_username"],
      email: ["email", "email_verified"],
    },

    scopes: ["openid", "profile", "email", "offline_access"],

    findAccount: async (_ctx: unknown, id: string) => {
      await connectDB();
      const member = await (Member as any).findById(id);
      if (!member) return undefined;

      return {
        accountId: id,
        async claims(_use: string, _scope: string) {
          return {
            sub: id,
            name: member.name,
            preferred_username: member.name,
            email: member.email,
            email_verified: true,
          };
        },
      };
    },

    cookies: {
      keys: (process.env.OIDC_COOKIE_SECRET || "dev-cookie-secret").split(","),
    },

    ttl: {
      AccessToken: 3600, // 1 hour
      AuthorizationCode: 600, // 10 minutes
      RefreshToken: 14 * 24 * 60 * 60, // 14 days
      Session: 14 * 24 * 60 * 60, // 14 days
      Interaction: 900, // 15 minutes — must match magic-link JWT TTL so the interaction outlives the token
      Grant: 14 * 24 * 60 * 60, // 14 days
    },

    features: {
      devInteractions: {
        enabled: process.env.NODE_ENV !== "production",
      },
      revocation: { enabled: true },
      rpInitiatedLogout: {
        enabled: true,
        logoutSource: async (ctx: any, form: string) => {
          // Auto-submit oidc-provider's own form so the xsrf value stays
          // inside the same request cycle that generated it. The previous
          // approach extracted the xsrf into a separate cookie and bounced
          // through a Nuxt page for a "are you sure?" confirmation, which
          // kept desyncing and producing "xsrf token invalid" errors.
          // Clicking sign-out in the wiki is already confirmation enough.
          ctx.type = "html";
          ctx.status = 200;
          ctx.body = `<!DOCTYPE html>
<html>
<head>
  <title>Signing out — Ghost Guild</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    html, body { margin: 0; padding: 0; height: 100%; background: #1a1814; color: #e8d9b8; font-family: "Commit Mono", ui-monospace, monospace; }
    body { display: grid; place-items: center; }
    p { font-size: 13px; letter-spacing: 0.05em; text-transform: uppercase; color: #b09c76; }
    form { display: none; }
  </style>
</head>
<body>
  <p>Signing you out…</p>
  ${form}
  <script>document.getElementById('op.logoutForm').submit()</script>
</body>
</html>`;
        },
        postLogoutSuccessSource: async (ctx: any) => {
          // Kill the Ghost Guild session cookie so the user is fully signed
          // out, not just logged out of Outline.
          ctx.cookies.set("auth-token", null, {
            httpOnly: true,
            sameSite: "lax",
            path: "/",
            overwrite: true,
            signed: false,
          });
          ctx.redirect("/auth/logout-success");
        },
      },
    },

    // Mount all OIDC endpoints under /oidc prefix
    routes: {
      authorization: "/oidc/auth",
      backchannel_authentication: "/oidc/backchannel",
      code_verification: "/oidc/device",
      device_authorization: "/oidc/device/auth",
      end_session: "/oidc/session/end",
      introspection: "/oidc/token/introspection",
      jwks: "/oidc/jwks",
      pushed_authorization_request: "/oidc/request",
      registration: "/oidc/reg",
      revocation: "/oidc/token/revocation",
      token: "/oidc/token",
      userinfo: "/oidc/me",
    },

    interactions: {
      url(_ctx: unknown, interaction: { uid: string }) {
        return `/oidc/interaction/${interaction.uid}`;
      },
    },

    renderError: async (ctx: any, out: Record<string, string>, _error: Error) => {
      // Allow-list only the standard OIDC error response fields. Prevents
      // leaking internal error messages / stack traces, keeps the query
      // string short, and the Nuxt page escapes them on render via Vue's
      // default interpolation (fixes the prior XSS via unescaped HTML
      // interpolation in the old guildPageShell implementation).
      const params = new URLSearchParams();
      if (out.error) params.set("error", out.error);
      if (out.error_description) params.set("error_description", out.error_description);
      ctx.redirect(`/auth/oidc-error?${params.toString()}`);
    },

    // Allow Outline to use PKCE but don't require it
    pkce: {
      required: () => false,
    },

    // Skip consent for our first-party Outline client
    loadExistingGrant: async (ctx: any) => {
      const grant = new (ctx.oidc.provider.Grant as any)({
        accountId: ctx.oidc.session!.accountId,
        clientId: ctx.oidc.client!.clientId,
      });
      grant.addOIDCScope("openid profile email");
      await grant.save();
      return grant;
    },
  });

  // oidc-provider extends Koa but calls super() with no args, so app.proxy
  // defaults to false — which makes ctx.protocol ignore X-Forwarded-Proto and
  // emit http:// URLs for form actions, discovery metadata, authorization
  // redirects, etc. Setting proxy = true here makes Koa trust Traefik's
  // X-Forwarded-Proto header and build https:// URLs in production.
  (_provider as any).proxy = true;

  return _provider;
}
