import YAML from "yaml";
import { DyadError, DyadErrorKind } from "@/errors/dyad_error";
import type { ConnectorEndpoint } from "@/db/schema";

const OPENAPI_METHODS = [
  "get",
  "post",
  "put",
  "patch",
  "delete",
  "options",
  "head",
] as const;

type OpenApiLikeDocument = {
  openapi?: string;
  swagger?: string;
  info?: {
    title?: string;
    description?: string;
  };
  paths?: Record<string, Record<string, Record<string, unknown>>>;
};

function parseSpecText(rawText: string): Record<string, unknown> {
  try {
    return JSON.parse(rawText) as Record<string, unknown>;
  } catch {
    try {
      const parsed = YAML.parse(rawText);
      if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
        throw new Error("OpenAPI document must be an object");
      }
      return parsed as Record<string, unknown>;
    } catch (error) {
      throw new DyadError(
        `Failed to parse OpenAPI document: ${
          error instanceof Error ? error.message : String(error)
        }`,
        DyadErrorKind.Validation,
      );
    }
  }
}

function buildEndpointId(method: string, path: string, operationId?: string) {
  return operationId?.trim() || `${method.toUpperCase()} ${path}`;
}

export function extractOpenApiEndpoints(
  document: Record<string, unknown>,
): ConnectorEndpoint[] {
  const spec = document as OpenApiLikeDocument;
  if (!spec.paths || typeof spec.paths !== "object") {
    throw new DyadError(
      "OpenAPI document does not contain any paths.",
      DyadErrorKind.Validation,
    );
  }

  const endpoints: ConnectorEndpoint[] = [];

  for (const [path, operations] of Object.entries(spec.paths)) {
    if (!operations || typeof operations !== "object") continue;
    for (const method of OPENAPI_METHODS) {
      const operation = operations[method];
      if (!operation || typeof operation !== "object") continue;

      const summary =
        typeof operation.summary === "string" ? operation.summary : null;
      const description =
        typeof operation.description === "string"
          ? operation.description
          : null;
      const operationId =
        typeof operation.operationId === "string"
          ? operation.operationId
          : null;
      const tags = Array.isArray(operation.tags)
        ? operation.tags.filter((tag): tag is string => typeof tag === "string")
        : [];

      endpoints.push({
        id: buildEndpointId(method, path, operationId ?? undefined),
        method: method.toUpperCase(),
        path,
        operationId,
        summary,
        description,
        tags,
      });
    }
  }

  if (endpoints.length === 0) {
    throw new DyadError(
      "No API endpoints were found in the OpenAPI document.",
      DyadErrorKind.Validation,
    );
  }

  return endpoints.sort((a, b) => {
    const pathCompare = a.path.localeCompare(b.path);
    if (pathCompare !== 0) return pathCompare;
    return a.method.localeCompare(b.method);
  });
}

export async function fetchAndParseOpenApiDocument(url: string): Promise<{
  rawSpec: Record<string, unknown>;
  name: string;
  description: string | null;
  specVersion: string | null;
  endpoints: ConnectorEndpoint[];
}> {
  let response: Response;
  try {
    response = await fetch(url);
  } catch (error) {
    throw new DyadError(
      `Failed to fetch OpenAPI document: ${
        error instanceof Error ? error.message : String(error)
      }`,
      DyadErrorKind.External,
    );
  }

  if (!response.ok) {
    throw new DyadError(
      `Failed to fetch OpenAPI document (${response.status} ${response.statusText}).`,
      DyadErrorKind.External,
    );
  }

  const rawText = await response.text();
  const rawSpec = parseSpecText(rawText);
  const spec = rawSpec as OpenApiLikeDocument;
  const specVersion =
    typeof spec.openapi === "string"
      ? spec.openapi
      : typeof spec.swagger === "string"
        ? spec.swagger
        : null;

  if (!specVersion) {
    throw new DyadError(
      "This document is not a valid Swagger/OpenAPI specification.",
      DyadErrorKind.Validation,
    );
  }

  const endpoints = extractOpenApiEndpoints(rawSpec);
  const name =
    typeof spec.info?.title === "string" && spec.info.title.trim().length > 0
      ? spec.info.title.trim()
      : new URL(url).hostname;
  const description =
    typeof spec.info?.description === "string" ? spec.info.description : null;

  return {
    rawSpec,
    name,
    description,
    specVersion,
    endpoints,
  };
}

export function formatOpenApiConnectorSystemPrompt(input: {
  name: string;
  sourceUrl: string;
  description: string | null;
  specVersion: string | null;
  endpoints: ConnectorEndpoint[];
}): string {
  const endpointLines = input.endpoints
    .slice(0, 60)
    .map((endpoint) => {
      const summary = endpoint.summary?.trim()
        ? ` - ${endpoint.summary.trim()}`
        : "";
      const operationId = endpoint.operationId
        ? ` (${endpoint.operationId})`
        : "";
      return `- ${endpoint.method} ${endpoint.path}${operationId}${summary}`;
    })
    .join("\n");

  const remainingCount = Math.max(input.endpoints.length - 60, 0);
  const descriptionBlock =
    input.description && input.description.trim().length > 0
      ? `Description: ${input.description.trim()}\n`
      : "";

  return `# API Connector
This app is connected to an external API specification that you should use as the source of truth when generating API calls, client helpers, request shapes, and integration code.

Connector: ${input.name}
Spec URL: ${input.sourceUrl}
Spec Version: ${input.specVersion ?? "unknown"}
${descriptionBlock}Available endpoints:
${endpointLines}
${remainingCount > 0 ? `- ...and ${remainingCount} more endpoints` : ""}

Prefer these endpoints over inventing new API routes. If you need to wire this API into the app, generate code that calls these external endpoints rather than creating fake local endpoints unless the user explicitly asks for a mock or proxy layer.`;
}
