import "server-only";

import { and, eq, or, sql } from "drizzle-orm";
import type { Conn } from "@/lib/db/raw";
import { withUserContext, type Tx } from "@/lib/db/rls";
import { projects, tasks, taskEdges, type NewTaskEdge } from "@/lib/db/schema";
import type { EdgeType, HistoryEntry } from "@/lib/types";
import { asIdentifier, composeTaskRef } from "@/lib/graph/identifier";
import { fetchDependencyChain } from "@/lib/db/raw/fetch-dependency-chain";
import { appendTaskHistoryMany } from "@/lib/data/task";
import { formatMarkdown } from "@/lib/markdown/format";
import type { AuthContext } from "@/lib/auth/context";
import {
  ForbiddenError,
  assertTaskAccessTx,
  isUuid,
} from "@/lib/auth/authorization";
import { emitEdgeMutation } from "@/lib/realtime/events";

/**
 * Build a timestamped history entry.
 * @param entry - Partial entry without id/date.
 * @returns Complete history entry with generated id and current date.
 */
function makeHistoryEntry(
  entry: Omit<HistoryEntry, "id" | "date">,
): HistoryEntry {
  return {
    ...entry,
    id: crypto.randomUUID(),
    date: new Date().toISOString(),
  };
}

// ---------------------------------------------------------------------------
// Edge queries
// ---------------------------------------------------------------------------

/**
 * Look up an edge by (source, target, type) when the caller can access
 * both endpoints. Returns the row, or null when no such edge exists.
 * @param ctx - Resolved auth context.
 * @param sourceTaskId - UUID of the source task.
 * @param targetTaskId - UUID of the target task.
 * @param edgeType - Edge relationship type.
 * @throws ForbiddenError when either endpoint is cross-team.
 */
export async function findEdgeByNodes(
  ctx: AuthContext,
  sourceTaskId: string,
  targetTaskId: string,
  edgeType: EdgeType,
) {
  return withUserContext(ctx.userId, async (tx) => {
    await Promise.all([
      assertTaskAccessTx(tx, sourceTaskId),
      assertTaskAccessTx(tx, targetTaskId),
    ]);
    const [row] = await tx
      .select()
      .from(taskEdges)
      .where(
        and(
          eq(taskEdges.sourceTaskId, sourceTaskId),
          eq(taskEdges.targetTaskId, targetTaskId),
          eq(taskEdges.edgeType, edgeType),
        ),
      )
      .limit(1);
    return row ?? null;
  });
}

/**
 * Fetch all edges where a task is source or target.
 * @param ctx - Resolved auth context.
 * @param taskId - UUID of the task.
 * @returns Array of edges.
 */
export async function getTaskEdges(ctx: AuthContext, taskId: string) {
  return withUserContext(ctx.userId, async (tx) => {
    await assertTaskAccessTx(tx, taskId);
    return tx
      .select()
      .from(taskEdges)
      .where(
        or(
          eq(taskEdges.sourceTaskId, taskId),
          eq(taskEdges.targetTaskId, taskId),
        ),
      );
  });
}

// ---------------------------------------------------------------------------
// Task edges with details
// ---------------------------------------------------------------------------

/** An edge with full connected task details. */
export type DetailedEdge = {
  edgeId: string;
  edgeType: EdgeType;
  direction: "outgoing" | "incoming";
  note: string;
  connectedTask: {
    id: string;
    taskRef: string;
    title: string;
    status: string;
  };
};

/**
 * Fetch all edges on a task with connected task titles and statuses.
 * @param ctx - Resolved auth context.
 * @param taskId - UUID of the task.
 * @returns Array of detailed edges.
 */
export async function getTaskEdgesDetailed(
  ctx: AuthContext,
  taskId: string,
): Promise<DetailedEdge[]> {
  return withUserContext(ctx.userId, (tx) =>
    getTaskEdgesDetailedTx(tx, taskId),
  );
}

/**
 * Same contract as {@link getTaskEdgesDetailed} but runs on a
 * caller-supplied transaction handle so context builders can share one
 * `withUserContext` frame across the access check, the edge fetch, and
 * the surrounding work.
 *
 * @param tx - Drizzle transaction handle from an active `withUserContext` frame.
 * @param taskId - UUID of the task.
 * @returns Array of detailed edges.
 */
export async function getTaskEdgesDetailedTx(
  tx: Tx,
  taskId: string,
): Promise<DetailedEdge[]> {
  await assertTaskAccessTx(tx, taskId);

  const edges = await tx
    .select()
    .from(taskEdges)
    .where(
      or(
        eq(taskEdges.sourceTaskId, taskId),
        eq(taskEdges.targetTaskId, taskId),
      ),
    );

  const idsToFetch = new Set<string>();
  for (const edge of edges) {
    const isOutgoing = edge.sourceTaskId === taskId;
    idsToFetch.add(isOutgoing ? edge.targetTaskId : edge.sourceTaskId);
  }

  const taskInfoMap = new Map<
    string,
    { taskRef: string; title: string; status: string }
  >();

  if (idsToFetch.size > 0) {
    const ids = [...idsToFetch];
    const taskRows = await tx
      .select({
        id: tasks.id,
        title: tasks.title,
        status: tasks.status,
        sequenceNumber: tasks.sequenceNumber,
        identifier: projects.identifier,
      })
      .from(tasks)
      .innerJoin(projects, eq(tasks.projectId, projects.id))
      .where(sql`${tasks.id} IN ${ids}`);
    for (const t of taskRows) {
      taskInfoMap.set(t.id, {
        taskRef: composeTaskRef(asIdentifier(t.identifier), t.sequenceNumber),
        title: t.title,
        status: t.status,
      });
    }
  }

  return edges
    .map((edge) => {
      const isOutgoing = edge.sourceTaskId === taskId;
      const connectedId = isOutgoing ? edge.targetTaskId : edge.sourceTaskId;
      const info = taskInfoMap.get(connectedId);
      if (!info) return null;
      return {
        edgeId: edge.id,
        edgeType: edge.edgeType,
        direction: isOutgoing ? ("outgoing" as const) : ("incoming" as const),
        note: edge.note,
        connectedTask: { id: connectedId, ...info },
      };
    })
    .filter((e): e is DetailedEdge => e !== null);
}

// ---------------------------------------------------------------------------
// Graph helpers
// ---------------------------------------------------------------------------

/**
 * Fetch every edge whose source OR target task is in the supplied id set.
 * Internal helper for the project-overview context assembler — caller
 * asserted access on the project, and the id set is the project's task
 * ids so the OR-on-id-list is safely scoped.
 *
 * @param taskIds - Task ids to filter the endpoints on.
 * @param conn - RLS-scoped {@link Conn} from an active `withUserContext` frame.
 * @returns Full edge rows.
 */
export async function fetchEdgesForTaskIds(taskIds: string[], conn: Conn) {
  if (taskIds.length === 0) return [];
  return conn
    .select()
    .from(taskEdges)
    .where(
      or(
        sql`${taskEdges.sourceTaskId} IN ${taskIds}`,
        sql`${taskEdges.targetTaskId} IN ${taskIds}`,
      ),
    );
}

/**
 * Fetch every `depends_on` edge whose source task is in the supplied
 * id set. Used by graph algorithms.
 *
 * @param sourceTaskIds - Task ids to filter the source side on.
 * @param conn - RLS-scoped {@link Conn} from an active `withUserContext` frame.
 * @returns Edge endpoints (source/target only — no metadata).
 */
export async function listDependsOnEdges(sourceTaskIds: string[], conn: Conn) {
  if (sourceTaskIds.length === 0) return [];
  return conn
    .select({
      sourceTaskId: taskEdges.sourceTaskId,
      targetTaskId: taskEdges.targetTaskId,
    })
    .from(taskEdges)
    .where(
      and(
        sql`${taskEdges.sourceTaskId} IN ${sourceTaskIds}`,
        eq(taskEdges.edgeType, "depends_on"),
      ),
    );
}

// ---------------------------------------------------------------------------
// Edge mutations
// ---------------------------------------------------------------------------

/**
 * Create an edge between two tasks and append history to both.
 * Validates against self-edges, duplicates, and circular depends_on.
 * @param ctx - Resolved auth context.
 * @param data - Edge fields to insert.
 * @returns The created edge.
 * @throws Error if validation fails.
 */
export async function createEdge(
  ctx: AuthContext,
  data: Omit<NewTaskEdge, "id">,
) {
  if (data.sourceTaskId === data.targetTaskId) {
    throw new Error(
      "Cannot create self-edge: source and target are the same task.",
    );
  }

  if (typeof data.note === "string" && data.note.trim()) {
    data = { ...data, note: (await formatMarkdown(data.note)) ?? data.note };
  }

  const { edge, projectId } = await withUserContext(ctx.userId, async (tx) => {
    const [sourceTask, targetTask] = await Promise.all([
      assertTaskAccessTx(tx, data.sourceTaskId),
      assertTaskAccessTx(tx, data.targetTaskId),
    ]);

    if (sourceTask.projectId !== targetTask.projectId) {
      throw new Error(
        "Cannot create edge between tasks in different projects.",
      );
    }

    const [existing] = await tx
      .select({ id: taskEdges.id })
      .from(taskEdges)
      .where(
        and(
          eq(taskEdges.sourceTaskId, data.sourceTaskId),
          eq(taskEdges.targetTaskId, data.targetTaskId),
          eq(taskEdges.edgeType, data.edgeType),
        ),
      );
    if (existing) {
      throw new Error("Duplicate edge: an identical edge already exists.");
    }

    if (data.edgeType === "depends_on") {
      const chain = await fetchDependencyChain(
        tx,
        data.targetTaskId,
        targetTask.projectId,
        10,
      );
      if (chain.some((node) => node.id === data.sourceTaskId)) {
        throw new Error(
          "Circular dependency: adding this edge would create a cycle.",
        );
      }
    }

    const [created] = await tx.insert(taskEdges).values(data).returning();

    const historyEntry = makeHistoryEntry({
      type: "edge_added",
      label: `Edge: ${data.edgeType}`,
      description: `${data.edgeType} edge created.`,
      actor: "ai",
    });

    await appendTaskHistoryMany(
      [data.sourceTaskId, data.targetTaskId],
      historyEntry,
      { tx },
    );

    return { edge: created, projectId: sourceTask.projectId };
  });

  emitEdgeMutation(projectId, data.sourceTaskId, data.targetTaskId);
  return {
    id: edge.id,
    sourceTaskId: edge.sourceTaskId,
    targetTaskId: edge.targetTaskId,
    edgeType: edge.edgeType,
    note: edge.note,
  };
}

/**
 * Fetch an edge and assert caller access via the parent project on a
 * supplied tx. Missing edge and cross-team access both surface as
 * `ForbiddenError({ resource: "edge" })`.
 *
 * @param tx - Active RLS transaction handle.
 * @param edgeId - UUID of the edge.
 * @returns The edge row and its parent project id.
 * @throws ForbiddenError on missing edge, malformed id, or cross-team access.
 */
async function loadAuthorizedEdgeTx(tx: Tx, edgeId: string) {
  if (!isUuid(edgeId)) {
    throw new ForbiddenError("Forbidden", "edge", edgeId);
  }
  const [edge] = await tx
    .select()
    .from(taskEdges)
    .where(eq(taskEdges.id, edgeId));
  if (!edge) throw new ForbiddenError("Forbidden", "edge", edgeId);
  let sourceTask;
  try {
    sourceTask = await assertTaskAccessTx(tx, edge.sourceTaskId);
  } catch (err) {
    if (err instanceof ForbiddenError) {
      throw new ForbiddenError("Forbidden", "edge", edgeId);
    }
    throw err;
  }
  return { edge, projectId: sourceTask.projectId };
}

/**
 * Update an edgeType and/or note. Endpoints are immutable through this
 * helper, so a cycle check is only needed on a type change INTO `depends_on`.
 *
 * @param ctx - Resolved auth context.
 * @param edgeId - UUID of the edge to update.
 * @param updates - Fields to update.
 * @returns The updated edge.
 * @throws Error if edge not found or validation fails.
 */
export async function updateEdge(
  ctx: AuthContext,
  edgeId: string,
  updates: { edgeType?: EdgeType; note?: string },
) {
  if (typeof updates.note === "string" && updates.note.trim()) {
    updates = {
      ...updates,
      note: (await formatMarkdown(updates.note)) ?? updates.note,
    };
  }

  const setClause: Record<string, unknown> = { updatedAt: new Date() };
  if (updates.edgeType !== undefined) setClause.edgeType = updates.edgeType;
  if (updates.note !== undefined) setClause.note = updates.note;

  const { updated, existing, projectId } = await withUserContext(
    ctx.userId,
    async (tx) => {
      const { edge: existing, projectId } = await loadAuthorizedEdgeTx(
        tx,
        edgeId,
      );

      let targetProjectIdForCycle: string | undefined;
      if (
        updates.edgeType &&
        updates.edgeType !== existing.edgeType &&
        updates.edgeType === "depends_on"
      ) {
        const targetTask = await assertTaskAccessTx(tx, existing.targetTaskId);
        targetProjectIdForCycle = targetTask.projectId;
      }

      if (updates.edgeType && updates.edgeType !== existing.edgeType) {
        const [dup] = await tx
          .select({ id: taskEdges.id })
          .from(taskEdges)
          .where(
            and(
              eq(taskEdges.sourceTaskId, existing.sourceTaskId),
              eq(taskEdges.targetTaskId, existing.targetTaskId),
              eq(taskEdges.edgeType, updates.edgeType),
            ),
          );
        if (dup) {
          throw new Error(
            "Duplicate edge: an edge with this type already exists between these tasks.",
          );
        }
      }

      if (targetProjectIdForCycle) {
        const chain = await fetchDependencyChain(
          tx,
          existing.targetTaskId,
          targetProjectIdForCycle,
          10,
        );
        if (chain.some((node) => node.id === existing.sourceTaskId)) {
          throw new Error(
            "Circular dependency: changing this edge type would create a cycle.",
          );
        }
      }

      const [row] = await tx
        .update(taskEdges)
        .set(setClause)
        .where(eq(taskEdges.id, edgeId))
        .returning();

      const historyEntry = makeHistoryEntry({
        type: "edge_updated",
        label: `Edge updated: ${row.edgeType}`,
        description: `Edge updated${updates.edgeType ? ` to ${updates.edgeType}` : ""}${
          updates.note !== undefined ? " with new note" : ""
        }.`,
        actor: "ai",
      });

      await appendTaskHistoryMany(
        [existing.sourceTaskId, existing.targetTaskId],
        historyEntry,
        { tx },
      );

      return { updated: row, existing, projectId };
    },
  );

  emitEdgeMutation(projectId, existing.sourceTaskId, existing.targetTaskId);
  return {
    id: updated.id,
    sourceTaskId: updated.sourceTaskId,
    targetTaskId: updated.targetTaskId,
    edgeType: updated.edgeType,
    note: updated.note,
  };
}

/**
 * Remove an edge by ID and append history to both tasks.
 * @param ctx - Resolved auth context.
 * @param edgeId - UUID of the edge to delete.
 */
export async function removeEdge(ctx: AuthContext, edgeId: string) {
  const { edge, projectId } = await withUserContext(ctx.userId, async (tx) => {
    const { edge, projectId } = await loadAuthorizedEdgeTx(tx, edgeId);

    await tx.delete(taskEdges).where(eq(taskEdges.id, edgeId));

    const historyEntry = makeHistoryEntry({
      type: "edge_removed",
      label: `Edge removed: ${edge.edgeType}`,
      description: `${edge.edgeType} edge removed.`,
      actor: "user",
    });

    await appendTaskHistoryMany(
      [edge.sourceTaskId, edge.targetTaskId],
      historyEntry,
      { tx },
    );

    return { edge, projectId };
  });

  emitEdgeMutation(projectId, edge.sourceTaskId, edge.targetTaskId);
}
