import { and, desc, eq } from "drizzle-orm";
import { db } from "../../db";
import { chats, connectors } from "../../db/schema";
import { createTypedHandler } from "./base";
import { connectorContracts } from "../types/connector";
import {
  fetchAndParseOpenApiDocument,
  extractOpenApiEndpoints,
} from "../utils/openapi_utils";
import { DyadError, DyadErrorKind } from "@/errors/dyad_error";

function toConnectorSummary(
  connector: typeof connectors.$inferSelect,
  endpointCount?: number,
) {
  return {
    id: connector.id,
    name: connector.name,
    type: connector.type,
    sourceUrl: connector.sourceUrl,
    specVersion: connector.specVersion,
    description: connector.description,
    endpointCount: endpointCount ?? connector.endpointsJson?.length ?? 0,
    lastSyncedAt: connector.lastSyncedAt,
    createdAt: connector.createdAt,
    updatedAt: connector.updatedAt,
  };
}

function toConnectorDetail(connector: typeof connectors.$inferSelect) {
  return {
    ...toConnectorSummary(connector),
    rawSpec: connector.rawSpec,
    endpoints: connector.endpointsJson ?? [],
  };
}

function hasLegacyAppConnectorColumn(): boolean {
  type TableInfoRow = {
    name?: string;
  };

  const rows = db.$client
    .prepare("PRAGMA table_info(apps)")
    .all() as TableInfoRow[];

  return rows.some((row) => row.name === "connector_id");
}

async function removeConnectorIdFromChats(connectorId: number): Promise<void> {
  const existingChats = await db.query.chats.findMany({
    columns: {
      id: true,
      connectorIdsJson: true,
    },
  });

  for (const chat of existingChats) {
    const connectorIds = chat.connectorIdsJson ?? [];
    if (!connectorIds.includes(connectorId)) {
      continue;
    }

    const filteredConnectorIds = connectorIds.filter(
      (id) => id !== connectorId,
    );
    await db
      .update(chats)
      .set({
        connectorIdsJson:
          filteredConnectorIds.length > 0 ? filteredConnectorIds : null,
      })
      .where(eq(chats.id, chat.id));
  }
}

export async function deleteConnectorWithCleanup(
  connectorId: number,
): Promise<void> {
  const connector = await db.query.connectors.findFirst({
    where: eq(connectors.id, connectorId),
  });

  if (!connector) {
    throw new DyadError("Connector not found", DyadErrorKind.NotFound);
  }

  try {
    if (hasLegacyAppConnectorColumn()) {
      db.$client
        .prepare("UPDATE apps SET connector_id = NULL WHERE connector_id = ?")
        .run(connectorId);
    }

    await removeConnectorIdFromChats(connectorId);
    await db.delete(connectors).where(eq(connectors.id, connectorId));
  } catch (error) {
    throw new DyadError(
      error instanceof Error
        ? `Failed to delete connector: ${error.message}`
        : "Failed to delete connector",
      DyadErrorKind.External,
    );
  }
}

export function registerConnectorHandlers() {
  createTypedHandler(connectorContracts.listConnectors, async () => {
    const allConnectors = await db.query.connectors.findMany({
      orderBy: [desc(connectors.updatedAt)],
    });

    return {
      connectors: allConnectors.map((connector) =>
        toConnectorSummary(connector),
      ),
    };
  });

  createTypedHandler(connectorContracts.getConnector, async (_, params) => {
    const connector = await db.query.connectors.findFirst({
      where: eq(connectors.id, params.connectorId),
    });

    if (!connector) {
      throw new DyadError("Connector not found", DyadErrorKind.NotFound);
    }

    return toConnectorDetail(connector);
  });

  createTypedHandler(
    connectorContracts.importOpenApiConnector,
    async (_, params) => {
      const imported = await fetchAndParseOpenApiDocument(params.url);
      const now = new Date();

      const connectorName =
        params.name?.trim() && params.name.trim().length > 0
          ? params.name.trim()
          : imported.name;

      const existing = await db.query.connectors.findFirst({
        where: and(
          eq(connectors.sourceUrl, params.url),
          eq(connectors.type, "openapi"),
        ),
      });

      if (existing) {
        await db
          .update(connectors)
          .set({
            name: connectorName,
            description: imported.description,
            specVersion: imported.specVersion,
            rawSpec: imported.rawSpec,
            endpointsJson: imported.endpoints,
            lastSyncedAt: now,
            updatedAt: now,
          })
          .where(eq(connectors.id, existing.id));

        const updated = await db.query.connectors.findFirst({
          where: eq(connectors.id, existing.id),
        });

        if (!updated) {
          throw new DyadError(
            "Connector disappeared during update.",
            DyadErrorKind.Internal,
          );
        }

        return toConnectorDetail(updated);
      }

      const result = await db
        .insert(connectors)
        .values({
          name: connectorName,
          type: "openapi",
          sourceUrl: params.url,
          description: imported.description,
          specVersion: imported.specVersion,
          rawSpec: imported.rawSpec,
          endpointsJson: imported.endpoints,
          lastSyncedAt: now,
          updatedAt: now,
        })
        .returning();

      const created = result[0];
      if (!created) {
        throw new DyadError(
          "Failed to create connector.",
          DyadErrorKind.Internal,
        );
      }

      return toConnectorDetail(created);
    },
  );

  createTypedHandler(connectorContracts.refreshConnector, async (_, params) => {
    const connector = await db.query.connectors.findFirst({
      where: eq(connectors.id, params.connectorId),
    });

    if (!connector) {
      throw new DyadError("Connector not found", DyadErrorKind.NotFound);
    }

    const imported = await fetchAndParseOpenApiDocument(connector.sourceUrl);
    const now = new Date();

    await db
      .update(connectors)
      .set({
        name: connector.name,
        description: imported.description,
        specVersion: imported.specVersion,
        rawSpec: imported.rawSpec,
        endpointsJson: imported.endpoints,
        lastSyncedAt: now,
        updatedAt: now,
      })
      .where(eq(connectors.id, connector.id));

    const refreshed = await db.query.connectors.findFirst({
      where: eq(connectors.id, connector.id),
    });

    if (!refreshed) {
      throw new DyadError(
        "Connector disappeared during refresh.",
        DyadErrorKind.Internal,
      );
    }

    return toConnectorDetail(refreshed);
  });

  createTypedHandler(connectorContracts.deleteConnector, async (_, params) => {
    await deleteConnectorWithCleanup(params.connectorId);
  });
}

export { extractOpenApiEndpoints };
