import { Hono } from "hono";
import { and, asc, desc, eq, inArray, isNull } from "drizzle-orm";
import { z } from "zod";

import { diagram } from "@turbostarter/db/schema";
import { db } from "@turbostarter/db/server";
import { HttpStatusCode } from "@turbostarter/shared/constants";
import { HttpException } from "@turbostarter/shared/utils";

import { enforceAuth, validate } from "../../middleware";

export const listDiagramsQuerySchema = z.object({
  projectId: z.string().optional(),
  unorganized: z.enum(["true"]).optional(),
});

export const createDiagramSchema = z.object({
  title: z.string().min(1).max(255),
  type: z.enum([
    "bpmn",
    "er",
    "orgchart",
    "architecture",
    "sequence",
    "flowchart",
  ]),
  projectId: z.string().optional(),
});

export const updateDiagramBodySchema = z
  .object({
    title: z.string().min(1).max(255).optional(),
    projectId: z.string().nullable().optional(),
    sortOrder: z.number().int().min(0).optional(),
  })
  .refine(
    (data) =>
      data.title !== undefined ||
      data.projectId !== undefined ||
      data.sortOrder !== undefined,
    { message: "At least one field must be provided" },
  );

export const reorderDiagramsSchema = z
  .object({
    items: z
      .array(
        z.object({
          id: z.string(),
          sortOrder: z.number().int().min(0),
        }),
      )
      .min(1)
      .max(100),
  })
  .refine(
    (data) => new Set(data.items.map((i) => i.id)).size === data.items.length,
    { message: "Duplicate diagram IDs not allowed" },
  );

/**
 * Fetch a diagram by ID and verify ownership.
 * Returns 404 if not found, 403 if not owned by userId.
 * Future: also accept valid share tokens (Epic 6).
 */
async function getOwnedDiagram(diagramId: string, userId: string) {
  const [d] = await db
    .select()
    .from(diagram)
    .where(
      and(
        eq(diagram.id, diagramId),
        isNull(diagram.deletedAt),
      ),
    );

  if (!d) {
    throw new HttpException(HttpStatusCode.NOT_FOUND, {
      code: "error.notFound",
    });
  }

  if (d.userId !== userId) {
    throw new HttpException(HttpStatusCode.FORBIDDEN, {
      code: "error.forbidden",
      message: "You don't have access to this diagram",
    });
  }

  return d;
}

export const diagramRouter = new Hono()
  .get("/", enforceAuth, validate("query", listDiagramsQuerySchema), async (c) => {
    const { projectId, unorganized } = c.req.valid("query");

    const conditions = [
      eq(diagram.userId, c.var.user.id),
      isNull(diagram.deletedAt),
    ];

    if (projectId) {
      conditions.push(eq(diagram.projectId, projectId));
    } else if (unorganized === "true") {
      conditions.push(isNull(diagram.projectId));
    }

    const orderClauses = projectId
      ? [asc(diagram.sortOrder), desc(diagram.updatedAt)]
      : [desc(diagram.updatedAt)];

    const diagrams = await db
      .select()
      .from(diagram)
      .where(and(...conditions))
      .orderBy(...orderClauses);

    return c.json({ data: diagrams });
  })
  .post(
    "/reorder",
    enforceAuth,
    validate("json", reorderDiagramsSchema),
    async (c) => {
      const { items } = c.req.valid("json");

      const diagramIds = items.map((i) => i.id);
      const owned = await db
        .select({ id: diagram.id })
        .from(diagram)
        .where(
          and(
            inArray(diagram.id, diagramIds),
            eq(diagram.userId, c.var.user.id),
            isNull(diagram.deletedAt),
          ),
        );

      if (owned.length !== diagramIds.length) {
        throw new HttpException(HttpStatusCode.FORBIDDEN, {
          code: "error.forbidden",
          message: "One or more diagrams not found or not owned",
        });
      }

      await db.transaction(async (tx) => {
        for (const item of items) {
          await tx
            .update(diagram)
            .set({ sortOrder: item.sortOrder })
            .where(eq(diagram.id, item.id));
        }
      });

      return c.json({ data: { success: true } });
    },
  )
  .get("/:id", enforceAuth, async (c) => {
    const d = await getOwnedDiagram(c.req.param("id"), c.var.user.id);
    return c.json({ data: d });
  })
  .post(
    "/",
    enforceAuth,
    validate("json", createDiagramSchema),
    async (c) => {
      const input = c.req.valid("json");
      const [created] = await db
        .insert(diagram)
        .values({
          ...input,
          userId: c.var.user.id,
        })
        .returning();

      return c.json({ data: created });
    },
  )
  .patch(
    "/:id",
    enforceAuth,
    validate("json", updateDiagramBodySchema),
    async (c) => {
      await getOwnedDiagram(c.req.param("id"), c.var.user.id);

      const [updated] = await db
        .update(diagram)
        .set(c.req.valid("json"))
        .where(eq(diagram.id, c.req.param("id")))
        .returning();

      return c.json({ data: updated });
    },
  )
  .delete("/:id", enforceAuth, async (c) => {
    await getOwnedDiagram(c.req.param("id"), c.var.user.id);

    await db
      .update(diagram)
      .set({ deletedAt: new Date() })
      .where(eq(diagram.id, c.req.param("id")));

    return c.json({ data: { success: true } });
  });
