// ---------------------------------------------------------------------------
// Shared OAuth HTTP API — one endpoint set per flow, served at
// `/scopes/:scopeId/oauth/{probe,start,complete,callback}` for every
// plugin that needs OAuth. `pluginId` lives on the request body so the
// completion callback can route to the right plugin at persist time.
// Replaces the four per-plugin copies that lived under
// `/scopes/:scopeId/{mcp,openapi,graphql,google-discovery}/oauth/*`.
// ---------------------------------------------------------------------------

import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi";
import { Schema } from "effect";

import {
  InternalError,
  OAuthCompleteError,
  OAuthProbeError,
  OAuthSessionNotFoundError,
  OAuthStartError,
  OAuthStrategySchema,
  ScopeId,
  SecretBackedMap,
} from "@executor-js/sdk/shared";

const ScopeParams = { scopeId: ScopeId };
// ---------------------------------------------------------------------------
// Probe — decide between dynamic-DCR and paste-your-credentials flows
// ---------------------------------------------------------------------------

const ProbePayload = Schema.Struct({
  endpoint: Schema.String,
  headers: Schema.optional(SecretBackedMap),
  queryParams: Schema.optional(SecretBackedMap),
});

const ProbeResponse = Schema.Struct({
  resourceMetadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)),
  resourceMetadataUrl: Schema.NullOr(Schema.String),
  authorizationServerMetadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)),
  authorizationServerMetadataUrl: Schema.NullOr(Schema.String),
  authorizationServerUrl: Schema.NullOr(Schema.String),
  supportsDynamicRegistration: Schema.Boolean,
  isBearerChallengeEndpoint: Schema.Boolean,
});

// ---------------------------------------------------------------------------
// Start — persists an `oauth2_session` row; for user-interactive flows
// returns an authorization URL, for `client-credentials` mints the
// Connection inline and returns it under `completedConnection`.
// ---------------------------------------------------------------------------

const StartPayload = Schema.Struct({
  /** Resource URL — used by probe/display, not by the start flow for
   *  static strategies. */
  endpoint: Schema.String,
  headers: Schema.optional(SecretBackedMap),
  queryParams: Schema.optional(SecretBackedMap),
  /** Where the authorization server will bounce the user's browser
   *  back to. Pass a placeholder (e.g. the token URL) for flows that
   *  don't redirect; the service still persists it. */
  redirectUrl: Schema.String,
  /** Stable id the Connection the exchange will mint. Caller typically
   *  derives this as `${pluginId}-oauth2-${namespace}` so the source
   *  row can be stamped atomically with the flow start. */
  connectionId: Schema.String,
  /** Scope where the resulting Connection + its backing secrets land. */
  tokenScope: Schema.String,
  strategy: OAuthStrategySchema,
  /** Which plugin is initiating the flow. Persisted on the session +
   *  stamped on the minted Connection's identity-label prefix. */
  pluginId: Schema.String,
  /** Human label for the minted Connection. */
  identityLabel: Schema.optional(Schema.String),
});

const StartResponse = Schema.Struct({
  sessionId: Schema.String,
  /** Present for user-interactive strategies. `null` for
   *  `client-credentials` (no redirect). */
  authorizationUrl: Schema.NullOr(Schema.String),
  /** Filled for strategies that mint the Connection inline. */
  completedConnection: Schema.NullOr(Schema.Struct({ connectionId: Schema.String })),
});

// ---------------------------------------------------------------------------
// Complete — exchange the code, mint the Connection, drop the session.
// ---------------------------------------------------------------------------

const CompletePayload = Schema.Struct({
  state: Schema.String,
  code: Schema.optional(Schema.String),
  error: Schema.optional(Schema.String),
});

const CompleteResponse = Schema.Struct({
  connectionId: Schema.String,
  expiresAt: Schema.NullOr(Schema.Number),
  scope: Schema.NullOr(Schema.String),
});

// ---------------------------------------------------------------------------
// Cancel — drop an in-flight session without exchanging.
// ---------------------------------------------------------------------------

const CancelPayload = Schema.Struct({
  sessionId: Schema.String,
  /** Scope that owns the pending OAuth session. Must match start.tokenScope. */
  tokenScope: Schema.String,
});

const CancelResponse = Schema.Struct({
  cancelled: Schema.Boolean,
});

// ---------------------------------------------------------------------------
// OAuth callback — GET with `state` + `code` (or `error`) query params.
// Renders the popup HTML directly; the popup script posts the completion
// result back to the opener via `postMessage` / `BroadcastChannel`.
// ---------------------------------------------------------------------------

const CallbackUrlParams = Schema.Struct({
  state: Schema.String,
  code: Schema.optional(Schema.String),
  error: Schema.optional(Schema.String),
  error_description: Schema.optional(Schema.String),
});

const HtmlResponse = Schema.String.pipe(HttpApiSchema.asText());

// ---------------------------------------------------------------------------
// Group
// ---------------------------------------------------------------------------

export const OAuthApi = HttpApiGroup.make("oauth")
  .add(
    HttpApiEndpoint.post("probe", "/scopes/:scopeId/oauth/probe", {
      params: ScopeParams,
      payload: ProbePayload,
      success: ProbeResponse,
      error: [InternalError, OAuthProbeError],
    }),
  )
  .add(
    HttpApiEndpoint.post("start", "/scopes/:scopeId/oauth/start", {
      params: ScopeParams,
      payload: StartPayload,
      success: StartResponse,
      error: [InternalError, OAuthStartError],
    }),
  )
  .add(
    HttpApiEndpoint.post("complete", "/scopes/:scopeId/oauth/complete", {
      params: ScopeParams,
      payload: CompletePayload,
      success: CompleteResponse,
      error: [InternalError, OAuthCompleteError, OAuthSessionNotFoundError],
    }),
  )
  .add(
    HttpApiEndpoint.post("cancel", "/scopes/:scopeId/oauth/cancel", {
      params: ScopeParams,
      payload: CancelPayload,
      success: CancelResponse,
      error: [InternalError, OAuthSessionNotFoundError],
    }),
  )
  .add(
    HttpApiEndpoint.get("callback", "/oauth/callback", {
      query: CallbackUrlParams,
      success: HtmlResponse,
      error: [InternalError, OAuthCompleteError, OAuthSessionNotFoundError],
    }),
  );
