import { Effect, Layer, Option, Schema } from "effect";
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";

import type { PluginCtx, StorageFailure } from "@executor-js/sdk/core";

import { GoogleDiscoveryInvocationError, GoogleDiscoveryOAuthError } from "./errors";
import type { GoogleDiscoveryStore } from "./binding-store";
import {
  GoogleDiscoveryInvocationResult,
  type GoogleDiscoveryParameter,
  type GoogleDiscoveryStoredSourceData,
} from "./types";

const SAFE_METHODS = new Set(["get", "head", "options"]);

const UnknownErrorMessage = Schema.Struct({ message: Schema.String });
const decodeUnknownErrorMessage = Schema.decodeUnknownOption(UnknownErrorMessage);

const errorMessageFromUnknown = (cause: unknown): string => {
  const decoded = decodeUnknownErrorMessage(cause);
  if (Option.isSome(decoded)) return decoded.value.message;
  // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: preserves existing fallback text for HTTP client errors
  return String(cause);
};

export const annotationsForOperation = (
  method: string,
  pathTemplate: string,
): { requiresApproval?: boolean; approvalDescription?: string } => {
  if (SAFE_METHODS.has(method.toLowerCase())) return {};
  return {
    requiresApproval: true,
    approvalDescription: `${method.toUpperCase()} ${pathTemplate}`,
  };
};

// ---------------------------------------------------------------------------
// Path / query parameter helpers (unchanged from the old invoker)
// ---------------------------------------------------------------------------

const stringValuesFromParameter = (value: unknown, repeated: boolean): string[] => {
  if (value === undefined || value === null) return [];
  if (Array.isArray(value)) {
    const normalized = value.flatMap((entry) =>
      entry === undefined || entry === null ? [] : [String(entry)],
    );
    return repeated ? normalized : [normalized.join(",")];
  }
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
    return [String(value)];
  }
  return [JSON.stringify(value)];
};

const replacePathParameters = (input: {
  pathTemplate: string;
  args: Record<string, unknown>;
  parameters: readonly GoogleDiscoveryParameter[];
}): Effect.Effect<string, GoogleDiscoveryInvocationError> =>
  Effect.gen(function* () {
    let failure: GoogleDiscoveryInvocationError | undefined;
    const resolved = input.pathTemplate.replaceAll(/\{([^}]+)\}/g, (_, name: string) => {
      const parameter = input.parameters.find(
        (entry) => entry.location === "path" && entry.name === name,
      );
      const values = stringValuesFromParameter(input.args[name], false);
      if (values.length === 0) {
        if (parameter?.required) {
          failure = new GoogleDiscoveryInvocationError({
            message: `Missing required path parameter: ${name}`,
            statusCode: Option.none(),
          });
        }
        return "";
      }
      return encodeURIComponent(values[0]!);
    });
    if (failure) return yield* failure;
    return resolved;
  });

const resolveBaseUrl = (source: GoogleDiscoveryStoredSourceData): string =>
  new URL(source.servicePath || "", source.rootUrl).toString();

const isJsonContentType = (contentType: string | null | undefined): boolean => {
  if (!contentType) return false;
  const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
  return (
    normalized === "application/json" || normalized.includes("+json") || normalized.includes("json")
  );
};

// ---------------------------------------------------------------------------
// HTTP request builder / executor
// ---------------------------------------------------------------------------

const performRequest = Effect.fn("GoogleDiscovery.invoke")(function* (input: {
  method: string;
  pathTemplate: string;
  parameters: readonly GoogleDiscoveryParameter[];
  hasBody: boolean;
  source: GoogleDiscoveryStoredSourceData;
  args: Record<string, unknown>;
  authorizationHeader?: string;
}) {
  const client = yield* HttpClient.HttpClient;

  const resolvedPath = yield* replacePathParameters({
    pathTemplate: input.pathTemplate,
    args: input.args,
    parameters: input.parameters,
  });
  const requestUrl = new URL(resolvedPath.replace(/^\//, ""), resolveBaseUrl(input.source));

  for (const parameter of input.parameters) {
    if (parameter.location === "path") continue;
    const values = stringValuesFromParameter(input.args[parameter.name], parameter.repeated);
    if (values.length === 0) {
      if (parameter.required) {
        return yield* new GoogleDiscoveryInvocationError({
          message: `Missing required ${parameter.location} parameter: ${parameter.name}`,
          statusCode: Option.none(),
        });
      }
      continue;
    }
    if (parameter.location === "query") {
      for (const value of values) {
        requestUrl.searchParams.append(parameter.name, value);
      }
    }
  }

  let request = HttpClientRequest.make(input.method.toUpperCase() as "GET")(requestUrl.toString());

  for (const parameter of input.parameters) {
    if (parameter.location !== "header") continue;
    const values = stringValuesFromParameter(input.args[parameter.name], parameter.repeated);
    if (values.length === 0) continue;
    request = HttpClientRequest.setHeader(
      request,
      parameter.name,
      parameter.repeated ? values.join(",") : values[0]!,
    );
  }

  if (input.authorizationHeader) {
    request = HttpClientRequest.setHeader(request, "Authorization", input.authorizationHeader);
  }

  if (input.hasBody && input.args.body !== undefined) {
    request = HttpClientRequest.bodyJsonUnsafe(request, input.args.body);
  }

  const response = yield* client.execute(request).pipe(
    Effect.mapError(
      (err) =>
        new GoogleDiscoveryInvocationError({
          message: `HTTP request failed: ${errorMessageFromUnknown(err)}`,
          statusCode: Option.none(),
          cause: err,
        }),
    ),
  );

  const contentType = response.headers["content-type"] ?? null;
  const mapBodyError = Effect.mapError(
    (err: unknown) =>
      new GoogleDiscoveryInvocationError({
        message: `Failed to read response body: ${errorMessageFromUnknown(err)}`,
        statusCode: Option.some(response.status),
        cause: err,
      }),
  );
  const body =
    response.status === 204
      ? null
      : isJsonContentType(contentType)
        ? yield* response.json.pipe(
            Effect.catch(() => response.text),
            mapBodyError,
          )
        : yield* response.text.pipe(mapBodyError);

  const ok = response.status >= 200 && response.status < 300;
  return GoogleDiscoveryInvocationResult.make({
    status: response.status,
    headers: { ...response.headers },
    data: ok ? body : null,
    error: ok ? null : body,
  });
});

// ---------------------------------------------------------------------------
// Entry point — called from plugin.invokeTool.
// ---------------------------------------------------------------------------

export const invokeGoogleDiscoveryTool = (input: {
  ctx: PluginCtx<GoogleDiscoveryStore>;
  toolId: string;
  /** Resolved owning scope of the tool row. */
  toolScope: string;
  args: unknown;
  httpClientLayer?: Layer.Layer<HttpClient.HttpClient>;
}): Effect.Effect<
  GoogleDiscoveryInvocationResult,
  GoogleDiscoveryInvocationError | GoogleDiscoveryOAuthError | StorageFailure
> =>
  Effect.gen(function* () {
    const entry = yield* input.ctx.storage.getBinding(input.toolId, input.toolScope);
    if (!entry) {
      return yield* new GoogleDiscoveryInvocationError({
        message: `No Google Discovery operation found for tool "${input.toolId}"`,
        statusCode: Option.none(),
      });
    }
    const stored = yield* input.ctx.storage.getSource(entry.namespace, input.toolScope);
    if (!stored) {
      return yield* new GoogleDiscoveryInvocationError({
        message: `No Google Discovery source found for "${entry.namespace}"`,
        statusCode: Option.none(),
      });
    }
    const source = stored.config;

    const authHeader =
      source.auth.kind === "oauth2"
        ? `Bearer ${yield* input.ctx.connections.accessToken(source.auth.connectionId).pipe(
            Effect.mapError(
              (err) =>
                new GoogleDiscoveryOAuthError({
                  message: errorMessageFromUnknown(err),
                }),
            ),
          )}`
        : undefined;

    const layer = input.httpClientLayer ?? FetchHttpClient.layer;

    return yield* performRequest({
      method: entry.binding.method,
      pathTemplate: entry.binding.pathTemplate,
      parameters: entry.binding.parameters,
      hasBody: entry.binding.hasBody,
      source,
      args: (input.args ?? {}) as Record<string, unknown>,
      authorizationHeader: authHeader,
    }).pipe(Effect.provide(layer)) as Effect.Effect<
      GoogleDiscoveryInvocationResult,
      GoogleDiscoveryInvocationError,
      never
    >;
  });
