import { Effect, Match, Option } from "effect";

import { GraphqlExtractionError } from "./errors";
import type {
  IntrospectionResult,
  IntrospectionSchema,
  IntrospectionType,
  IntrospectionTypeRef,
  IntrospectionInputValue,
} from "./introspect";
import {
  ExtractedField,
  ExtractionResult,
  GraphqlArgument,
  type GraphqlOperationKind,
} from "./types";

// ---------------------------------------------------------------------------
// Type ref helpers
// ---------------------------------------------------------------------------

/** Unwrap NON_NULL / LIST wrappers to get the leaf type name */
const unwrapTypeName = (ref: IntrospectionTypeRef): string => {
  if (ref.name) return ref.name;
  if (ref.ofType) return unwrapTypeName(ref.ofType);
  return "Unknown";
};

/** Check if a type ref is non-null (required) */
const isNonNull = (ref: IntrospectionTypeRef): boolean => ref.kind === "NON_NULL";

// ---------------------------------------------------------------------------
// Build shared definitions from all INPUT_OBJECT and ENUM types
// ---------------------------------------------------------------------------

const buildDefinitions = (
  types: ReadonlyMap<string, IntrospectionType>,
): Record<string, unknown> => {
  const defs: Record<string, unknown> = {};

  for (const [name, type] of types) {
    // Skip internal types
    if (name.startsWith("__")) continue;

    if (type.kind === "INPUT_OBJECT" && type.inputFields) {
      const properties: Record<string, unknown> = {};
      const required: string[] = [];

      for (const field of type.inputFields) {
        const schema = typeRefToJsonSchema(field.type, types);
        if (field.description) {
          (schema as Record<string, unknown>).description = field.description;
        }
        properties[field.name] = schema;
        if (isNonNull(field.type)) {
          required.push(field.name);
        }
      }

      const def: Record<string, unknown> = { type: "object", properties };
      if (required.length > 0) def.required = required;
      if (type.description) def.description = type.description;
      defs[name] = def;
    }

    if (type.kind === "ENUM" && type.enumValues) {
      defs[name] = {
        type: "string",
        enum: type.enumValues.map((v) => v.name),
        ...(type.description ? { description: type.description } : {}),
      };
    }
  }

  return defs;
};

// ---------------------------------------------------------------------------
// Convert a type ref to JSON Schema using $ref for complex types
// ---------------------------------------------------------------------------

const typeRefToJsonSchema = (
  ref: IntrospectionTypeRef,
  // oxlint-disable-next-line only-used-in-recursion
  types: ReadonlyMap<string, IntrospectionType>,
): Record<string, unknown> =>
  Match.value(ref.kind).pipe(
    Match.when(
      "NON_NULL",
      (): Record<string, unknown> => (ref.ofType ? typeRefToJsonSchema(ref.ofType, types) : {}),
    ),
    Match.when(
      "LIST",
      (): Record<string, unknown> => ({
        type: "array",
        items: ref.ofType ? typeRefToJsonSchema(ref.ofType, types) : {},
      }),
    ),
    Match.when("SCALAR", (): Record<string, unknown> => scalarToJsonSchema(ref.name ?? "String")),
    Match.when(
      "ENUM",
      (): Record<string, unknown> =>
        ref.name ? { $ref: `#/$defs/${ref.name}` } : { type: "string" },
    ),
    Match.when(
      "INPUT_OBJECT",
      (): Record<string, unknown> =>
        ref.name ? { $ref: `#/$defs/${ref.name}` } : { type: "object" },
    ),
    Match.whenOr(
      "OBJECT",
      "INTERFACE",
      "UNION",
      (): Record<string, unknown> => ({ type: "object" }),
    ),
    Match.option,
    Option.getOrElse((): Record<string, unknown> => ({})),
  );

const scalarToJsonSchema = (name: string): Record<string, unknown> =>
  Match.value(name).pipe(
    Match.whenOr("String", "ID", (): Record<string, unknown> => ({ type: "string" })),
    Match.when("Int", (): Record<string, unknown> => ({ type: "integer" })),
    Match.when("Float", (): Record<string, unknown> => ({ type: "number" })),
    Match.when("Boolean", (): Record<string, unknown> => ({ type: "boolean" })),
    Match.option,
    Option.getOrElse(
      (): Record<string, unknown> => ({ type: "string", description: `Custom scalar: ${name}` }),
    ),
  );

// ---------------------------------------------------------------------------
// Build input JSON Schema from field arguments
// ---------------------------------------------------------------------------

const buildInputSchema = (
  args: readonly IntrospectionInputValue[],
  types: ReadonlyMap<string, IntrospectionType>,
): Record<string, unknown> | undefined => {
  if (args.length === 0) return undefined;

  const properties: Record<string, unknown> = {};
  const required: string[] = [];

  for (const arg of args) {
    const schema = typeRefToJsonSchema(arg.type, types);
    if (arg.description) {
      (schema as Record<string, unknown>).description = arg.description;
    }
    properties[arg.name] = schema;
    if (isNonNull(arg.type)) {
      required.push(arg.name);
    }
  }

  const inputSchema: Record<string, unknown> = {
    type: "object",
    properties,
  };
  if (required.length > 0) inputSchema.required = required;
  return inputSchema;
};

/** Format a type ref back to GraphQL type notation (e.g. "[String!]!") */
const formatTypeRef = (ref: IntrospectionTypeRef): string =>
  Match.value(ref.kind).pipe(
    Match.when("NON_NULL", () => (ref.ofType ? `${formatTypeRef(ref.ofType)}!` : "Unknown!")),
    Match.when("LIST", () => (ref.ofType ? `[${formatTypeRef(ref.ofType)}]` : "[Unknown]")),
    Match.option,
    Option.getOrElse(() => ref.name ?? "Unknown"),
  );

// ---------------------------------------------------------------------------
// Extract fields from schema
// ---------------------------------------------------------------------------

const extractFields = (
  _schema: IntrospectionSchema,
  kind: GraphqlOperationKind,
  typeName: string | null | undefined,
  types: ReadonlyMap<string, IntrospectionType>,
): ExtractedField[] => {
  if (!typeName) return [];

  const type = types.get(typeName);
  if (!type?.fields) return [];

  return type.fields
    .filter((f) => !f.name.startsWith("__"))
    .map((field) => {
      const args = field.args.map((arg) =>
        GraphqlArgument.make({
          name: arg.name,
          typeName: formatTypeRef(arg.type),
          required: isNonNull(arg.type),
          description: arg.description ? Option.some(arg.description) : Option.none(),
        }),
      );

      const inputSchema = buildInputSchema(field.args, types);

      return ExtractedField.make({
        fieldName: field.name,
        kind,
        description: field.description ? Option.some(field.description) : Option.none(),
        arguments: args,
        inputSchema: inputSchema ? Option.some(inputSchema) : Option.none(),
        returnTypeName: unwrapTypeName(field.type),
      });
    });
};

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

export interface ExtractionOutput {
  readonly result: ExtractionResult;
  /** Shared JSON Schema definitions for INPUT_OBJECT and ENUM types.
   *  Tool input schemas use `$ref` pointers into these. */
  readonly definitions: Record<string, unknown>;
}

export const extract = (
  introspection: IntrospectionResult,
): Effect.Effect<ExtractionOutput, GraphqlExtractionError> =>
  Effect.try({
    try: () => {
      const schema = introspection.__schema;
      const typeMap = new Map<string, IntrospectionType>();
      for (const t of schema.types) {
        typeMap.set(t.name, t);
      }

      const definitions = buildDefinitions(typeMap);

      const queryFields = extractFields(schema, "query", schema.queryType?.name, typeMap);
      const mutationFields = extractFields(schema, "mutation", schema.mutationType?.name, typeMap);

      return {
        result: ExtractionResult.make({
          schemaName: Option.none(),
          fields: [...queryFields, ...mutationFields],
        }),
        definitions,
      };
    },
    catch: () =>
      new GraphqlExtractionError({
        message: "Failed to extract GraphQL schema",
      }),
  });
