// ---------------------------------------------------------------------------
// Integrations HTTP API — the v2 catalog surface (was `sources`).
//
// An integration is the tenant-shared catalog identity (slug + description +
// which plugin owns it). The executor is bound to its `{ tenant, subject }` from
// the request auth, so integration routes carry no scope segment — the catalog
// is tenant-level. Connections (owner-scoped credentials) live in their own
// group; credential-binding endpoints are gone (folded into connections).
// ---------------------------------------------------------------------------

import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
import { Schema } from "effect";
import {
  HealthCheckCandidate,
  HealthCheckSpec,
  IntegrationDetectionResult,
  IntegrationNotFoundError,
  IntegrationRemovalNotAllowedError,
  IntegrationSlug,
  InternalError,
} from "@executor-js/sdk/shared";

// ---------------------------------------------------------------------------
// Params
// ---------------------------------------------------------------------------

const IntegrationParams = { slug: IntegrationSlug };

// ---------------------------------------------------------------------------
// Response / payload schemas
// ---------------------------------------------------------------------------

/** Where a credential value is carried — mirrors the SDK's
 *  `AuthPlacementDescriptor`. */
const PlacementDescriptor = Schema.Struct({
  carrier: Schema.Literals(["header", "query", "env"]),
  name: Schema.String,
  prefix: Schema.String,
  /** Input variable this placement renders from (absent ⇒ `token`). Without
   *  it the client cannot derive per-variable credential inputs for
   *  multi-input methods. */
  variable: Schema.optional(Schema.String),
  /** Static value rendered verbatim (no credential input). */
  literal: Schema.optional(Schema.String),
});

/** OAuth specifics — mirrors the SDK's `AuthMethodOAuthDescriptor`. */
const OAuthDescriptor = Schema.Struct({
  discoveryUrl: Schema.optional(Schema.String),
  authorizationUrl: Schema.optional(Schema.String),
  tokenUrl: Schema.optional(Schema.String),
  resource: Schema.optional(Schema.NullOr(Schema.String)),
  scopes: Schema.optional(Schema.Array(Schema.String)),
  registrationEndpoint: Schema.optional(Schema.String),
  supportsDynamicRegistration: Schema.optional(Schema.Boolean),
  supportsClientIdMetadataDocument: Schema.optional(Schema.Boolean),
});

/** A single declared auth method — mirrors the SDK's `AuthMethodDescriptor`. */
const AuthMethodDescriptorSchema = Schema.Struct({
  id: Schema.String,
  label: Schema.String,
  kind: Schema.Literals(["oauth", "apikey", "header", "none"]),
  template: Schema.String,
  placements: Schema.optional(Schema.Array(PlacementDescriptor)),
  oauth: Schema.optional(OAuthDescriptor),
});

/** Public projection of an integration — mirrors the SDK's `Integration`. */
const IntegrationResponse = Schema.Struct({
  slug: IntegrationSlug,
  /** Display name. */
  name: Schema.String,
  description: Schema.String,
  /** The plugin that owns this integration kind (e.g. "openapi", "mcp"). */
  kind: Schema.String,
  canRemove: Schema.Boolean,
  canRefresh: Schema.Boolean,
  /** Declared auth methods derived from the owning plugin's stored config.
   *  Always present (possibly empty) so the client never handles absence. */
  authMethods: Schema.Array(AuthMethodDescriptorSchema),
  /** Non-secret URL derived from opaque integration config for favicons. */
  displayUrl: Schema.optional(Schema.String),
});

const UpdateIntegrationPayload = Schema.Struct({
  name: Schema.optional(Schema.String),
  description: Schema.optional(Schema.String),
});

const DetectRequest = Schema.Struct({
  url: Schema.String.check(Schema.isMaxLength(2_048)),
});

// Set (or clear, with null) the integration's declared health check. The spec
// lives inside the owning plugin's opaque config; core only routes it.
const SetHealthCheckPayload = Schema.Struct({
  spec: Schema.NullOr(HealthCheckSpec),
});

// ---------------------------------------------------------------------------
// Error schemas with HTTP status annotations
// ---------------------------------------------------------------------------

const IntegrationNotFound = IntegrationNotFoundError.annotate({ httpApiStatus: 404 });
const IntegrationRemovalNotAllowed = IntegrationRemovalNotAllowedError.annotate({
  httpApiStatus: 409,
});

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

export const IntegrationsApi = HttpApiGroup.make("integrations")
  .add(
    HttpApiEndpoint.get("list", "/integrations", {
      success: Schema.Array(IntegrationResponse),
      error: InternalError,
    }),
  )
  .add(
    HttpApiEndpoint.get("get", "/integrations/:slug", {
      params: IntegrationParams,
      success: IntegrationResponse,
      error: [InternalError, IntegrationNotFound],
    }),
  )
  .add(
    HttpApiEndpoint.patch("update", "/integrations/:slug", {
      params: IntegrationParams,
      payload: UpdateIntegrationPayload,
      success: IntegrationResponse,
      error: [InternalError, IntegrationNotFound],
    }),
  )
  .add(
    HttpApiEndpoint.delete("remove", "/integrations/:slug", {
      params: IntegrationParams,
      success: Schema.Struct({ removed: Schema.Boolean }),
      error: [InternalError, IntegrationRemovalNotAllowed],
    }),
  )
  .add(
    HttpApiEndpoint.post("detect", "/integrations/detect", {
      payload: DetectRequest,
      success: Schema.Array(IntegrationDetectionResult),
      error: InternalError,
    }),
  )
  // The integration's currently declared health check (null when none is set).
  .add(
    HttpApiEndpoint.get("healthCheckGet", "/integrations/:slug/health-check", {
      params: IntegrationParams,
      success: Schema.NullOr(HealthCheckSpec),
      error: InternalError,
    }),
  )
  // Operations the user can pick as the health check, ranked non-destructive +
  // fewest-required-args first so the obvious identity endpoint floats to the top.
  .add(
    HttpApiEndpoint.get("healthCheckCandidates", "/integrations/:slug/health-check/candidates", {
      params: IntegrationParams,
      success: Schema.Array(HealthCheckCandidate),
      error: [InternalError, IntegrationNotFound],
    }),
  )
  // Persist (or clear) the integration's health check.
  .add(
    HttpApiEndpoint.put("healthCheckSet", "/integrations/:slug/health-check", {
      params: IntegrationParams,
      payload: SetHealthCheckPayload,
      success: Schema.Struct({ ok: Schema.Boolean }),
      error: [InternalError, IntegrationNotFound],
    }),
  );
