import { Effect, Option, Predicate } from "effect";
import { Schema } from "effect";

import {
  HealthCheckCandidate,
  compareHealthCheckCandidates,
  projectResponseFields,
} from "@executor-js/sdk/core";

import { parse, resolveSpecText, type ParsedDocument } from "./parse";
import { extract } from "./extract";
import { compileToolDefinitions } from "./definitions";
import { normalizeOpenApiRefs } from "./backing";
import { DocResolver } from "./openapi-utils";
import { HttpMethod, ServerInfo, type ExtractedOperation, type ExtractionResult } from "./types";

// Mutating HTTP methods: mirrors `REQUIRE_APPROVAL` in `./invoke` but kept
// inline so this browser-safe preview module never pulls in the HTTP execution
// path. A health check should be safe to re-run, so these rank last.
const DESTRUCTIVE_METHODS = new Set(["post", "put", "patch", "delete"]);

// Cap on health-check candidate METADATA carried in the preview, so the add
// screen's operation picker can search the whole spec (not just a top few).
// Building this is cheap (the rank/sort already runs over every operation); only
// the payload grows, so the cap bounds Graph-sized specs (16k+ ops) to the
// top-ranked 1000. Beyond that, the picker stays freeform (type an exact op).
const MAX_PREVIEW_CANDIDATES = 1000;

// Cap on how many carried candidates get their response schema WALKED for the
// typed identity picker. Walking is the only expensive part, so it stays small:
// the top-ranked survivors get typed identity fields; the long tail is
// metadata-only and its identity picker falls back to a freeform dot-path.
const MAX_PREVIEW_RESPONSE_FIELD_CANDIDATES = 50;

// ---------------------------------------------------------------------------
// OAuth 2.0 flows — one entry per supported grant type
// ---------------------------------------------------------------------------

/** Scopes declared by a flow: `{ scopeName: description }` */
const OAuth2Scopes = Schema.Record(Schema.String, Schema.String);
const SecuritySchemeType = Schema.Literals(["http", "apiKey", "oauth2", "openIdConnect"]);
type SecuritySchemeType = typeof SecuritySchemeType.Type;

const decodeSecuritySchemeType = Schema.decodeUnknownOption(SecuritySchemeType);

export const OAuth2AuthorizationCodeFlow = Schema.Struct({
  authorizationUrl: Schema.String,
  tokenUrl: Schema.String,
  refreshUrl: Schema.OptionFromOptional(Schema.String),
  scopes: OAuth2Scopes,
});
export type OAuth2AuthorizationCodeFlow = typeof OAuth2AuthorizationCodeFlow.Type;

export const OAuth2ClientCredentialsFlow = Schema.Struct({
  tokenUrl: Schema.String,
  refreshUrl: Schema.OptionFromOptional(Schema.String),
  scopes: OAuth2Scopes,
});
export type OAuth2ClientCredentialsFlow = typeof OAuth2ClientCredentialsFlow.Type;

export const OAuth2Flows = Schema.Struct({
  authorizationCode: Schema.OptionFromOptional(OAuth2AuthorizationCodeFlow),
  clientCredentials: Schema.OptionFromOptional(OAuth2ClientCredentialsFlow),
});
export type OAuth2Flows = typeof OAuth2Flows.Type;

// ---------------------------------------------------------------------------
// Security scheme — what the spec declares it needs
// ---------------------------------------------------------------------------

export const SecurityScheme = Schema.Struct({
  /** Key name in components.securitySchemes (e.g. "api_token") */
  name: Schema.String,
  /** OpenAPI security scheme type */
  type: SecuritySchemeType,
  /** For type: "http" — e.g. "bearer", "basic" */
  scheme: Schema.OptionFromOptional(Schema.String),
  /** For type: "http" with scheme "bearer" — e.g. "JWT" */
  bearerFormat: Schema.OptionFromOptional(Schema.String),
  /** For type: "apiKey" — where the key goes */
  in: Schema.OptionFromOptional(Schema.Literals(["header", "query", "cookie"])),
  /** For type: "apiKey" — the header/query/cookie name */
  headerName: Schema.OptionFromOptional(Schema.String),
  description: Schema.OptionFromOptional(Schema.String),
  /** For type: "oauth2" — declared flows (authorizationCode / clientCredentials only; implicit and password are deprecated). */
  flows: Schema.OptionFromOptional(OAuth2Flows),
  /** For type: "openIdConnect" — the discovery URL. */
  openIdConnectUrl: Schema.OptionFromOptional(Schema.String),
});
export type SecurityScheme = typeof SecurityScheme.Type;

// ---------------------------------------------------------------------------
// Auth strategy — a valid combination of security schemes
// ---------------------------------------------------------------------------

export const AuthStrategy = Schema.Struct({
  /** The security schemes required together for this strategy */
  schemes: Schema.Array(Schema.String),
});
export type AuthStrategy = typeof AuthStrategy.Type;

// ---------------------------------------------------------------------------
// Header preset — derived from an auth strategy
// ---------------------------------------------------------------------------

export const HeaderPreset = Schema.Struct({
  /** Human-readable label for the UI (e.g. "Bearer Token", "API Key + Email") */
  label: Schema.String,
  /** Headers this strategy needs. Value is null when the user must provide it. */
  headers: Schema.Record(Schema.String, Schema.NullOr(Schema.String)),
  /** Which headers should be stored as secrets */
  secretHeaders: Schema.Array(Schema.String),
});
export type HeaderPreset = typeof HeaderPreset.Type;

// ---------------------------------------------------------------------------
// OAuth2 preset — derived from an oauth2 security scheme + a flow choice
// ---------------------------------------------------------------------------

export const OAuth2Preset = Schema.Struct({
  /** Human-readable label for the UI (e.g. "OAuth2 (Authorization Code) — oauth_app") */
  label: Schema.String,
  /** The source security scheme this preset came from (components.securitySchemes key). */
  securitySchemeName: Schema.String,
  /** Which OAuth2 flow this preset uses. */
  flow: Schema.Literals(["authorizationCode", "clientCredentials"]),
  /** For authorizationCode: user-agent redirect URL (from the spec). */
  authorizationUrl: Schema.OptionFromOptional(Schema.String),
  /** Token endpoint to exchange the code / refresh. */
  tokenUrl: Schema.String,
  /** RFC 8707 resource indicator discovered from protected-resource metadata. */
  resource: Schema.OptionFromOptional(Schema.String),
  /** Optional refresh endpoint if the spec declares one separately. */
  refreshUrl: Schema.OptionFromOptional(Schema.String),
  /** Declared scopes for this flow: `{ scope: description }`. */
  scopes: Schema.Record(Schema.String, Schema.String),
  /** Identity scopes to request alongside API scopes. `"auto"` discovers standard OIDC scopes. */
  identityScopes: Schema.Union([
    Schema.Literal("auto"),
    Schema.Literal(false),
    Schema.Array(Schema.String),
  ]),
  /** Provider metadata advertised Client ID Metadata Document support. */
  supportsClientIdMetadataDocument: Schema.optional(Schema.Boolean),
});
export type OAuth2Preset = typeof OAuth2Preset.Type;

// ---------------------------------------------------------------------------
// Preview operation — lightweight shape for the add-source UI list
// ---------------------------------------------------------------------------

export const PreviewOperation = Schema.Struct({
  operationId: Schema.String,
  method: HttpMethod,
  path: Schema.String,
  summary: Schema.OptionFromOptional(Schema.String),
  tags: Schema.Array(Schema.String),
  deprecated: Schema.Boolean,
});
export type PreviewOperation = typeof PreviewOperation.Type;

// ---------------------------------------------------------------------------
// Spec preview — everything the frontend needs
// ---------------------------------------------------------------------------

export const SpecPreview = Schema.Struct({
  title: Schema.OptionFromOptional(Schema.String),
  /** The spec's `info.description` — prefills the add form's description field. */
  description: Schema.OptionFromOptional(Schema.String),
  version: Schema.OptionFromOptional(Schema.String),
  /** Reuses ServerInfo from extraction */
  servers: Schema.Array(ServerInfo),
  operationCount: Schema.Number,
  /** Lightweight operation list for the add-source UI */
  operations: Schema.Array(PreviewOperation),
  tags: Schema.Array(Schema.String),
  securitySchemes: Schema.Array(SecurityScheme),
  /** Valid auth strategies (each is a set of schemes used together) */
  authStrategies: Schema.Array(AuthStrategy),
  /** Pre-built header presets derived from auth strategies */
  headerPresets: Schema.Array(HeaderPreset),
  /** OAuth2 presets — one per (oauth2 scheme × supported flow) combination */
  oauth2Presets: Schema.Array(OAuth2Preset),
  /** Top-ranked health-check candidates (bounded), so the add screen can offer a
   *  typed operation + identity picker before the integration is registered. */
  healthCheckCandidates: Schema.Array(HealthCheckCandidate),
});
export type SpecPreview = typeof SpecPreview.Type;

// HTTP/UI preview deliberately omits the per-operation list. Graph-sized specs
// can define 16k+ operations, while the add flow only needs counts, tags,
// servers, and auth metadata before registration.
export const SpecPreviewSummary = Schema.Struct({
  title: Schema.OptionFromOptional(Schema.String),
  description: Schema.OptionFromOptional(Schema.String),
  version: Schema.OptionFromOptional(Schema.String),
  servers: Schema.Array(ServerInfo),
  operationCount: Schema.Number,
  tags: Schema.Array(Schema.String),
  securitySchemes: Schema.Array(SecurityScheme),
  authStrategies: Schema.Array(AuthStrategy),
  headerPresets: Schema.Array(HeaderPreset),
  oauth2Presets: Schema.Array(OAuth2Preset),
  healthCheckCandidates: Schema.Array(HealthCheckCandidate),
});
export type SpecPreviewSummary = typeof SpecPreviewSummary.Type;

export const specPreviewSummary = (preview: SpecPreview): SpecPreviewSummary =>
  SpecPreviewSummary.make({
    title: preview.title,
    description: preview.description,
    version: preview.version,
    servers: preview.servers,
    operationCount: preview.operationCount,
    tags: preview.tags,
    securitySchemes: preview.securitySchemes,
    authStrategies: preview.authStrategies,
    headerPresets: preview.headerPresets,
    oauth2Presets: preview.oauth2Presets,
    healthCheckCandidates: preview.healthCheckCandidates,
  });

// ---------------------------------------------------------------------------
// Security scheme extraction
// ---------------------------------------------------------------------------

const stringRecord = (value: unknown): Record<string, string> => {
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
  const out: Record<string, string> = {};
  for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
    if (typeof v === "string") out[k] = v;
  }
  return out;
};

const extractFlows = (rawFlows: unknown): Option.Option<OAuth2Flows> => {
  if (!rawFlows || typeof rawFlows !== "object") return Option.none();
  const flows = rawFlows as Record<string, unknown>;

  const parseFlow = <K extends "authorizationCode" | "clientCredentials">(key: K): unknown =>
    flows[key];

  let authorizationCode: Option.Option<OAuth2AuthorizationCodeFlow> = Option.none();
  const authCodeRaw = parseFlow("authorizationCode");
  if (authCodeRaw && typeof authCodeRaw === "object") {
    const f = authCodeRaw as Record<string, unknown>;
    const authUrl = typeof f.authorizationUrl === "string" ? f.authorizationUrl : null;
    const tokenUrl = typeof f.tokenUrl === "string" ? f.tokenUrl : null;
    if (authUrl && tokenUrl) {
      authorizationCode = Option.some(
        OAuth2AuthorizationCodeFlow.make({
          authorizationUrl: authUrl,
          tokenUrl,
          refreshUrl: Option.fromNullishOr(
            typeof f.refreshUrl === "string" ? f.refreshUrl : undefined,
          ),
          scopes: stringRecord(f.scopes),
        }),
      );
    }
  }

  let clientCredentials: Option.Option<OAuth2ClientCredentialsFlow> = Option.none();
  const ccRaw = parseFlow("clientCredentials");
  if (ccRaw && typeof ccRaw === "object") {
    const f = ccRaw as Record<string, unknown>;
    const tokenUrl = typeof f.tokenUrl === "string" ? f.tokenUrl : null;
    if (tokenUrl) {
      clientCredentials = Option.some(
        OAuth2ClientCredentialsFlow.make({
          tokenUrl,
          refreshUrl: Option.fromNullishOr(
            typeof f.refreshUrl === "string" ? f.refreshUrl : undefined,
          ),
          scopes: stringRecord(f.scopes),
        }),
      );
    }
  }

  if (Option.isNone(authorizationCode) && Option.isNone(clientCredentials)) {
    return Option.none();
  }
  return Option.some(OAuth2Flows.make({ authorizationCode, clientCredentials }));
};

const extractSecuritySchemes = (
  rawSchemes: Record<string, unknown>,
  resolver: DocResolver,
): SecurityScheme[] =>
  Object.entries(rawSchemes).flatMap(([name, schemeOrRef]) => {
    if (!schemeOrRef || typeof schemeOrRef !== "object") return [];
    // Resolve $ref so schemes defined via `$ref` aren't silently dropped.
    const resolved = resolver.resolve<Record<string, unknown>>(
      schemeOrRef as Record<string, unknown>,
    );
    if (!resolved || typeof resolved !== "object") return [];
    const scheme = resolved;

    const type = decodeSecuritySchemeType(scheme.type);
    if (Option.isNone(type)) return [];
    const schemeType = type.value;

    return [
      SecurityScheme.make({
        name,
        type: schemeType,
        scheme: Option.fromNullishOr(scheme.scheme as string | undefined),
        bearerFormat: Option.fromNullishOr(scheme.bearerFormat as string | undefined),
        in: Option.fromNullishOr(scheme.in as "header" | "query" | "cookie" | undefined),
        headerName: Option.fromNullishOr(scheme.name as string | undefined),
        description: Option.fromNullishOr(scheme.description as string | undefined),
        flows: schemeType === "oauth2" ? extractFlows(scheme.flows) : Option.none(),
        openIdConnectUrl: Option.fromNullishOr(scheme.openIdConnectUrl as string | undefined),
      }),
    ];
  });

// ---------------------------------------------------------------------------
// Header preset builder
// ---------------------------------------------------------------------------

const buildHeaderPresets = (
  schemes: readonly SecurityScheme[],
  strategies: readonly AuthStrategy[],
): HeaderPreset[] => {
  const schemeMap = new Map(schemes.map((s) => [s.name, s]));

  return strategies.flatMap((strategy) => {
    const resolved = strategy.schemes
      .map((name) => schemeMap.get(name))
      .filter(Predicate.isNotUndefined);

    if (resolved.length === 0) return [];

    const headers: Record<string, string | null> = {};
    const secretHeaders: string[] = [];
    const labelParts: string[] = [];

    for (const scheme of resolved) {
      if (scheme.type === "http" && Option.getOrElse(scheme.scheme, () => "") === "bearer") {
        headers["Authorization"] = null;
        secretHeaders.push("Authorization");
        labelParts.push("Bearer Token");
      } else if (scheme.type === "http" && Option.getOrElse(scheme.scheme, () => "") === "basic") {
        headers["Authorization"] = null;
        secretHeaders.push("Authorization");
        labelParts.push("Basic Auth");
      } else if (scheme.type === "apiKey" && Option.getOrElse(scheme.in, () => "") === "header") {
        const headerName = Option.getOrElse(scheme.headerName, () => scheme.name);
        headers[headerName] = null;
        secretHeaders.push(headerName);
        labelParts.push(scheme.name);
      } else if (scheme.type === "apiKey") {
        labelParts.push(`${scheme.name} (${Option.getOrElse(scheme.in, () => "unknown")})`);
      } else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") {
        return [];
      } else {
        labelParts.push(scheme.name);
      }
    }

    if (Object.keys(headers).length === 0 && resolved.length > 0) {
      return [
        HeaderPreset.make({
          label: labelParts.join(" + "),
          headers: {},
          secretHeaders: [],
        }),
      ];
    }

    return [
      HeaderPreset.make({
        label: labelParts.join(" + "),
        headers,
        secretHeaders,
      }),
    ];
  });
};

// ---------------------------------------------------------------------------
// OAuth2 preset builder
// ---------------------------------------------------------------------------

const buildOAuth2Presets = (schemes: readonly SecurityScheme[]): OAuth2Preset[] => {
  const presets: OAuth2Preset[] = [];
  for (const scheme of schemes) {
    if (scheme.type !== "oauth2") continue;
    if (Option.isNone(scheme.flows)) continue;
    const flows = scheme.flows.value;

    if (Option.isSome(flows.authorizationCode)) {
      const flow = flows.authorizationCode.value;
      presets.push(
        OAuth2Preset.make({
          label: `OAuth2 Authorization Code · ${scheme.name}`,
          securitySchemeName: scheme.name,
          flow: "authorizationCode",
          authorizationUrl: Option.some(flow.authorizationUrl),
          tokenUrl: flow.tokenUrl,
          resource: Option.none(),
          refreshUrl: flow.refreshUrl,
          scopes: flow.scopes,
          identityScopes: "auto",
        }),
      );
    }

    if (Option.isSome(flows.clientCredentials)) {
      const flow = flows.clientCredentials.value;
      presets.push(
        OAuth2Preset.make({
          label: `OAuth2 Client Credentials · ${scheme.name}`,
          securitySchemeName: scheme.name,
          flow: "clientCredentials",
          authorizationUrl: Option.none(),
          tokenUrl: flow.tokenUrl,
          resource: Option.none(),
          refreshUrl: flow.refreshUrl,
          scopes: flow.scopes,
          identityScopes: false,
        }),
      );
    }
  }
  return presets;
};

// ---------------------------------------------------------------------------
// Collect unique tags from extraction result
// ---------------------------------------------------------------------------

const collectTags = (result: ExtractionResult): string[] => {
  const tagSet = new Set<string>();
  for (const op of result.operations) {
    for (const tag of op.tags) tagSet.add(tag);
  }
  return [...tagSet].sort();
};

// ---------------------------------------------------------------------------
// Health-check candidates (bounded) for the add screen
// ---------------------------------------------------------------------------

/**
 * Project the top-ranked health-check candidates from a parsed doc + its
 * extracted operations, so the add screen can offer a typed operation/identity
 * picker before registration.
 *
 * Tool paths are computed on the FULL operation set (`compileToolDefinitions`
 * collision resolution is stateful, so they must match the paths the operations
 * get at registration). Candidates are ranked, sliced to `MAX_PREVIEW_CANDIDATES`
 * (enough for the operation picker to search the whole spec), and only the
 * top `MAX_PREVIEW_RESPONSE_FIELD_CANDIDATES` get their response schema walked for
 * the typed identity field; the rest stay metadata-only (freeform identity).
 */
const buildPreviewHealthCheckCandidates = (
  doc: ParsedDocument,
  operations: readonly ExtractedOperation[],
): HealthCheckCandidate[] => {
  if (operations.length === 0) return [];

  const definitions = compileToolDefinitions(operations);

  const ranked = definitions
    .map((def): HealthCheckCandidate => {
      const op = def.operation;
      const method = op.method.toLowerCase();
      const parameters = op.parameters.map((parameter) => ({
        name: parameter.name,
        location: parameter.location,
        required: parameter.required,
        ...(Option.isSome(parameter.description)
          ? { description: parameter.description.value }
          : {}),
      }));
      return {
        operation: def.toolPath,
        method,
        requiredArgCount: op.parameters.filter((parameter) => parameter.required).length,
        destructive: DESTRUCTIVE_METHODS.has(method),
        summary:
          Option.getOrUndefined(op.summary) ??
          Option.getOrUndefined(op.description) ??
          `${method.toUpperCase()} ${op.pathTemplate}`,
        ...(parameters.length > 0 ? { parameters } : {}),
      };
    })
    .sort(compareHealthCheckCandidates)
    .slice(0, MAX_PREVIEW_CANDIDATES);

  // Walk response schemas only for the top survivors (the realistic health-check
  // picks). `outputSchema` is NOT pre-normalized; the hoisted `$defs` ARE, so
  // normalize the schema first. The rest are returned metadata-only so the
  // operation picker still lists them while keeping schema walking bounded.
  const hoistedDefs: Record<string, unknown> = {};
  const rawSchemas = doc.components?.schemas;
  if (rawSchemas) {
    for (const [name, schema] of Object.entries(rawSchemas)) {
      hoistedDefs[name] = normalizeOpenApiRefs(schema);
    }
  }
  const operationByToolPath = new Map(definitions.map((def) => [def.toolPath, def.operation]));

  return ranked.map((candidate, index): HealthCheckCandidate => {
    if (index >= MAX_PREVIEW_RESPONSE_FIELD_CANDIDATES) return candidate;
    const op = operationByToolPath.get(candidate.operation);
    if (!op) return candidate;
    const responseFields = projectResponseFields(
      normalizeOpenApiRefs(Option.getOrUndefined(op.outputSchema)),
      hoistedDefs,
    );
    return responseFields.length > 0 ? { ...candidate, responseFields } : candidate;
  });
};

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/** Preview already-resolved spec text — extract metadata without registering
 *  anything and without any HTTP dependency. */
export const previewSpecText = Effect.fn("OpenApi.previewSpecText")(function* (specText: string) {
  const doc: ParsedDocument = yield* parse(specText);
  const result = yield* extract(doc);

  const resolver = new DocResolver(doc);
  const securitySchemes = extractSecuritySchemes(doc.components?.securitySchemes ?? {}, resolver);

  const rawSecurity = (doc.security ?? []) as Array<Record<string, unknown>>;
  const declaredStrategies = rawSecurity.map((entry) =>
    AuthStrategy.make({ schemes: Object.keys(entry) }),
  );
  // Fall back to one strategy per scheme when the spec only declares schemes
  // under components (e.g. Sentry) so the user still sees auth options.
  const authStrategies =
    declaredStrategies.length > 0
      ? declaredStrategies
      : securitySchemes.map((scheme) => AuthStrategy.make({ schemes: [scheme.name] }));

  return SpecPreview.make({
    title: result.title,
    description: result.description,
    version: result.version,
    servers: result.servers,
    operationCount: result.operations.length,
    operations: result.operations.map((op) =>
      PreviewOperation.make({
        operationId: op.operationId,
        method: op.method,
        path: op.pathTemplate,
        summary: op.summary,
        tags: op.tags,
        deprecated: op.deprecated,
      }),
    ),
    tags: collectTags(result),
    securitySchemes,
    authStrategies,
    headerPresets: buildHeaderPresets(securitySchemes, authStrategies),
    oauth2Presets: buildOAuth2Presets(securitySchemes),
    healthCheckCandidates: buildPreviewHealthCheckCandidates(doc, result.operations),
  });
});

/** Preview an OpenAPI spec — extract metadata without registering anything.
 *  Accepts either a URL or raw JSON/YAML text. */
export const previewSpec = Effect.fn("OpenApi.previewSpec")(function* (input: string) {
  const specText = yield* resolveSpecText(input);
  return yield* previewSpecText(specText);
});
