import { Effect } from "effect";
import type { Layer } from "effect";
import { HttpClient } from "effect/unstable/http";

import {
  IntegrationAlreadyExistsError,
  IntegrationDetectionResult,
  IntegrationNotFoundError,
  IntegrationSlug,
  definePlugin,
  mergeAuthTemplates,
  sha256Hex,
  type AuthMethodDescriptor,
  type Integration,
  type IntegrationConfig,
  type IntegrationRecord,
  type PluginCtx,
  type StorageFailure,
} from "@executor-js/sdk/core";
import { describeApiKeyAuthMethod } from "@executor-js/sdk/http-auth";
import {
  checkHealthOpenApi,
  compileAndPersistOpenApiSpecStreaming,
  listHealthCheckCandidatesOpenApi,
  decodeOpenApiIntegrationConfig,
  invokeOpenApiBackedTool,
  makeDefaultOpenapiStore,
  normalizeOpenApiAuthInputs,
  OpenApiExtractionError,
  resolveOpenApiBackedAnnotations,
  resolveOpenApiBackedTools,
  type Authentication,
  type AuthenticationInput,
  type OpenApiPersistResult,
  type OpenapiStore,
} from "@executor-js/plugin-openapi";

import {
  buildMicrosoftGraphOpenApiSpec,
  decodeMicrosoftGraphIntegrationConfig,
  microsoftGraphKeepPathItem,
  type MicrosoftGraphUrlPolicy,
  type MicrosoftGraphIntegrationConfig,
  type MicrosoftGraphSpecBuild,
} from "./graph";
import {
  MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG,
  MICROSOFT_GRAPH_BASE_URL,
  microsoftGraphPreset,
} from "./presets";

export interface MicrosoftGraphConfig {
  readonly presetIds?: readonly string[];
  readonly customScopes?: readonly string[];
  readonly slug?: string;
  readonly name?: string;
  readonly description?: string;
  readonly baseUrl?: string;
  readonly specUrl?: string;
  readonly authorizationUrl?: string;
  readonly tokenUrl?: string;
  readonly clientCredentialsTokenUrl?: string;
}

export interface MicrosoftConfigureInput {
  readonly authenticationTemplate: readonly AuthenticationInput[];
  readonly mode?: "merge" | "replace";
}

export interface MicrosoftUpdateInput {
  readonly presetIds?: readonly string[];
  readonly customScopes?: readonly string[];
  readonly baseUrl?: string;
  readonly specUrl?: string;
  readonly authorizationUrl?: string;
  readonly tokenUrl?: string;
  readonly clientCredentialsTokenUrl?: string;
}

export interface MicrosoftUpdateResult {
  readonly slug: IntegrationSlug;
  readonly toolCount: number;
  readonly addedTools: readonly string[];
  readonly removedTools: readonly string[];
}

export interface MicrosoftPluginOptions {
  readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;
  /**
   * Allows `addGraph` to point spec/base/OAuth URLs at a trusted https host
   * other than the pinned Microsoft Graph endpoints, or at plain http on
   * loopback (local Graph emulators). Off by default; hosts wire this to
   * their own local-network dev posture (e.g. `allowLocalNetwork`), never on
   * in production.
   */
  readonly allowUnsafeUrlOverrides?: boolean;
}

const DEFAULT_MICROSOFT_SLUG = "microsoft_graph";

const describeMicrosoftAuthMethods = (
  record: IntegrationRecord,
): readonly AuthMethodDescriptor[] => {
  const config = decodeOpenApiIntegrationConfig(record.config);
  if (!config) return [];
  return (config.authenticationTemplate ?? []).map(
    (template: Authentication): AuthMethodDescriptor => {
      if (template.kind === "oauth2") {
        const machineFlow =
          String(template.slug) === MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG;
        return {
          id: String(template.slug),
          label: machineFlow ? "Microsoft OAuth (client credentials)" : "Microsoft OAuth",
          kind: "oauth",
          template: String(template.slug),
          oauth: {
            authorizationUrl: template.authorizationUrl,
            tokenUrl: template.tokenUrl,
            scopes: template.scopes,
          },
        };
      }
      return describeApiKeyAuthMethod(template);
    },
  );
};

const describeMicrosoftIntegrationDisplay = (
  record: IntegrationRecord,
): { readonly url?: string } => {
  const config = decodeMicrosoftGraphIntegrationConfig(record.config);
  return { url: config?.baseUrl ?? MICROSOFT_GRAPH_BASE_URL };
};

const makeMicrosoftPluginExtension = (
  ctx: PluginCtx<OpenapiStore>,
  httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>,
  urlPolicy?: MicrosoftGraphUrlPolicy,
) => {
  const persistGraphOperations = (
    graph: MicrosoftGraphSpecBuild,
    integration: string,
    specHash: string,
  ): Effect.Effect<OpenApiPersistResult, OpenApiExtractionError | StorageFailure> =>
    // Stream the (full 37MB) source straight to persisted bindings + a
    // content-addressed defs blob, never materializing the whole-document tree
    // that OOMs the 128MB Workers isolate. `keepPathItem` applies the Microsoft
    // Graph scope selection per path-item during the stream; a full-graph
    // selection returns `undefined` (keep everything).
    compileAndPersistOpenApiSpecStreaming({
      specText: graph.specText,
      integration,
      storage: ctx.storage,
      specHash,
      keepPathItem: microsoftGraphKeepPathItem(graph),
    });

  const addGraph = (config: MicrosoftGraphConfig) =>
    Effect.gen(function* () {
      const graph = yield* buildMicrosoftGraphOpenApiSpec(config, httpClientLayer, urlPolicy);
      const slug = IntegrationSlug.make(config.slug?.trim() || DEFAULT_MICROSOFT_SLUG);

      const existing = yield* ctx.core.integrations.get(slug);
      if (existing) {
        return yield* new IntegrationAlreadyExistsError({ slug });
      }

      const specHash = yield* sha256Hex(graph.specText);
      const integrationConfig: MicrosoftGraphIntegrationConfig = {
        specHash,
        sourceUrl: graph.specUrl,
        microsoftGraphPresetIds: graph.presetIds,
        microsoftGraphCustomScopes: graph.customScopes,
        microsoftGraphScopes: graph.scopes,
        microsoftGraphExactPaths: graph.exactPaths,
        microsoftGraphPathPrefixes: graph.pathPrefixes,
        microsoftGraphTagPrefixes: graph.tagPrefixes,
        microsoftGraphCoversFullGraph: graph.coversFullGraph,
        microsoftGraphAuthorizationUrl: graph.authorizationUrl,
        microsoftGraphTokenUrl: graph.tokenUrl,
        microsoftGraphClientCredentialsTokenUrl: graph.clientCredentialsTokenUrl,
        authenticationTemplate: graph.authenticationTemplate,
        ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
      };

      yield* ctx.storage.putSpec(specHash, graph.specText);

      const persisted = yield* ctx.transaction(
        Effect.gen(function* () {
          yield* ctx.core.integrations.register({
            slug,
            name: config.name?.trim() || "Microsoft Graph",
            description: config.description ?? "Selected Microsoft Graph workloads.",
            config:
              integrationConfig satisfies MicrosoftGraphIntegrationConfig as IntegrationConfig,
            canRemove: true,
            canRefresh: true,
          });
          return yield* persistGraphOperations(graph, String(slug), specHash);
        }),
      );

      // Default the health check to `GET /me` (the canonical Graph identity
      // endpoint) when the selected workloads include it, so connections report
      // alive/expired + who-am-I out of the box. The operations were streamed
      // to storage above, so find the binding there. Best-effort: a Graph
      // selection without /me just leaves the check unconfigured (the editor
      // remains available).
      const meOperation = (yield* ctx.storage.listOperations(String(slug))).find(
        (op) => op.binding.method.toLowerCase() === "get" && op.binding.pathTemplate === "/me",
      );
      if (meOperation) {
        yield* ctx.core.integrations.setHealthCheck(slug, {
          operation: meOperation.toolName,
          identityField: "userPrincipalName",
        });
      }

      return { slug, toolCount: persisted.toolCount };
    });

  const updateGraph = (rawSlug: string, input?: MicrosoftUpdateInput) =>
    Effect.gen(function* () {
      const slug = IntegrationSlug.make(rawSlug);
      const record = yield* ctx.core.integrations.get(slug);
      const current = record ? decodeMicrosoftGraphIntegrationConfig(record.config) : null;
      if (!record || !current) {
        return yield* new IntegrationNotFoundError({ slug });
      }

      const graph = yield* buildMicrosoftGraphOpenApiSpec(
        {
          presetIds: input?.presetIds ?? current.microsoftGraphPresetIds,
          customScopes: input?.customScopes ?? current.microsoftGraphCustomScopes,
          baseUrl: input?.baseUrl ?? current.baseUrl,
          specUrl: input?.specUrl ?? current.sourceUrl,
          authorizationUrl: input?.authorizationUrl ?? current.microsoftGraphAuthorizationUrl,
          tokenUrl: input?.tokenUrl ?? current.microsoftGraphTokenUrl,
          clientCredentialsTokenUrl:
            input?.clientCredentialsTokenUrl ?? current.microsoftGraphClientCredentialsTokenUrl,
        },
        httpClientLayer,
        urlPolicy,
      );
      const previousOperations = yield* ctx.storage.listOperations(rawSlug);
      const previousNames = new Set(previousOperations.map((op) => op.toolName));

      const specHash = yield* sha256Hex(graph.specText);
      yield* ctx.storage.putSpec(specHash, graph.specText);

      const nextConfig: MicrosoftGraphIntegrationConfig = {
        ...current,
        specHash,
        sourceUrl: graph.specUrl,
        microsoftGraphPresetIds: graph.presetIds,
        microsoftGraphCustomScopes: graph.customScopes,
        microsoftGraphScopes: graph.scopes,
        microsoftGraphExactPaths: graph.exactPaths,
        microsoftGraphPathPrefixes: graph.pathPrefixes,
        microsoftGraphTagPrefixes: graph.tagPrefixes,
        microsoftGraphCoversFullGraph: graph.coversFullGraph,
        microsoftGraphAuthorizationUrl: graph.authorizationUrl,
        microsoftGraphTokenUrl: graph.tokenUrl,
        microsoftGraphClientCredentialsTokenUrl: graph.clientCredentialsTokenUrl,
        authenticationTemplate: graph.authenticationTemplate,
        ...(input?.baseUrl ? { baseUrl: input.baseUrl } : {}),
      };

      const persisted = yield* ctx.transaction(
        Effect.gen(function* () {
          yield* ctx.core.integrations.update(slug, {
            config: nextConfig satisfies MicrosoftGraphIntegrationConfig as IntegrationConfig,
          });
          return yield* persistGraphOperations(graph, rawSlug, specHash);
        }),
      );
      const nextNames = new Set(persisted.toolNames);

      const connections = yield* ctx.connections.list({ integration: slug });
      yield* Effect.forEach(
        connections,
        (connection) =>
          ctx.connections
            .refresh({
              owner: connection.owner,
              integration: connection.integration,
              name: connection.name,
            })
            .pipe(Effect.catchTag("ConnectionNotFoundError", () => Effect.succeed([]))),
        { discard: true },
      ).pipe(Effect.catchTag("IntegrationNotFoundError", () => Effect.void));

      return {
        slug,
        toolCount: persisted.toolCount,
        addedTools: [...nextNames].filter((name) => !previousNames.has(name)).sort(),
        removedTools: [...previousNames].filter((name) => !nextNames.has(name)).sort(),
      };
    });

  return {
    addGraph,
    updateGraph,
    removeGraph: (slug: string) =>
      ctx.transaction(
        Effect.gen(function* () {
          yield* ctx.storage.removeOperations(slug);
          yield* ctx.core.integrations
            .remove(IntegrationSlug.make(slug))
            .pipe(Effect.catchTag("IntegrationRemovalNotAllowedError", () => Effect.void));
        }),
      ),
    getIntegration: (slug: string) =>
      ctx.core.integrations.get(IntegrationSlug.make(slug)).pipe(
        Effect.map((record) =>
          record
            ? ({
                slug: record.slug,
                description: record.description,
                kind: record.kind,
                canRemove: record.canRemove,
                canRefresh: record.canRefresh,
              } as Integration)
            : null,
        ),
      ),
    getConfig: (slug: string) =>
      ctx.core.integrations
        .get(IntegrationSlug.make(slug))
        .pipe(
          Effect.map((record) =>
            record ? decodeMicrosoftGraphIntegrationConfig(record.config) : null,
          ),
        ),
    configure: (slug: string, input: MicrosoftConfigureInput) =>
      ctx.transaction(
        Effect.gen(function* () {
          const record = yield* ctx.core.integrations.get(IntegrationSlug.make(slug));
          if (!record) return [] as readonly Authentication[];
          const current = decodeMicrosoftGraphIntegrationConfig(record.config);
          if (!current) return [] as readonly Authentication[];

          const incoming = normalizeOpenApiAuthInputs(input.authenticationTemplate);
          const merged =
            input.mode === "replace"
              ? incoming
              : mergeAuthTemplates(current.authenticationTemplate ?? [], incoming);

          const next: MicrosoftGraphIntegrationConfig = {
            ...current,
            authenticationTemplate: merged,
          };

          yield* ctx.core.integrations.update(IntegrationSlug.make(slug), {
            config: next satisfies MicrosoftGraphIntegrationConfig as IntegrationConfig,
          });

          return merged;
        }),
      ),
  };
};

export type MicrosoftPluginExtension = ReturnType<typeof makeMicrosoftPluginExtension>;

export const microsoftPlugin = definePlugin((options?: MicrosoftPluginOptions) => ({
  id: "microsoft" as const,
  packageName: "@executor-js/plugin-microsoft",
  integrationPresets: [microsoftGraphPreset],
  storage: (deps): OpenapiStore => makeDefaultOpenapiStore(deps),

  extension: (ctx) =>
    makeMicrosoftPluginExtension(ctx, options?.httpClientLayer ?? ctx.httpClientLayer, {
      allowUnsafeUrlOverrides: options?.allowUnsafeUrlOverrides === true,
    }),

  describeAuthMethods: describeMicrosoftAuthMethods,
  describeIntegrationDisplay: describeMicrosoftIntegrationDisplay,

  resolveTools: ({ integration, config, storage }) =>
    resolveOpenApiBackedTools({ integration, config, storage }),

  invokeTool: ({ ctx, toolRow, credential, args }) => {
    const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
    return invokeOpenApiBackedTool({
      ctx,
      toolRow,
      credential,
      args,
      httpClientLayer,
    });
  },

  resolveAnnotations: ({ ctx, integration, toolRows }) =>
    resolveOpenApiBackedAnnotations({
      ctx,
      integration: String(integration),
      toolRows,
    }),

  // Health checks reuse the OpenAPI backing (same store). GET /me is
  // auto-defaulted at addGraph when present; core owns the stored spec.
  listHealthCheckCandidates: (input) =>
    listHealthCheckCandidatesOpenApi({ ctx: input.ctx, integration: input.integration }),
  checkHealth: (input) =>
    checkHealthOpenApi({
      ctx: input.ctx,
      integration: input.integration,
      credential: input.credential,
      spec: input.spec,
      httpClientLayer: options?.httpClientLayer ?? input.ctx.httpClientLayer,
    }),

  removeConnection: () => Effect.void,

  detect: ({ url }) =>
    Effect.sync(() => {
      const trimmed = url.trim();
      if (!URL.canParse(trimmed)) return null;
      const parsed = new URL(trimmed);
      const host = parsed.hostname.toLowerCase();
      const isGraph =
        host === "graph.microsoft.com" ||
        (host === "learn.microsoft.com" && parsed.pathname.startsWith("/graph/")) ||
        (host === "raw.githubusercontent.com" &&
          parsed.pathname.includes("/microsoftgraph/msgraph-metadata/"));
      if (!isGraph) return null;
      return IntegrationDetectionResult.make({
        kind: "microsoft",
        confidence: "high",
        endpoint: trimmed,
        name: "Microsoft Graph",
        slug: DEFAULT_MICROSOFT_SLUG,
      });
    }),
}));
