import { Router, type IRouter } from "express";
import { eq, and, desc, inArray } from "drizzle-orm";
import { db } from "@workspace/db";
import {
  notesTable,
  noteLabelsTable,
  noteLabelAssignmentsTable,
} from "@workspace/db";
import { requireAuth } from "../middlewares/auth";

const router: IRouter = Router();

function parseIdParam(raw: unknown): { ok: true; id: number } | { ok: false; error: string } {
  const n = Number(raw);
  if (!Number.isInteger(n) || n <= 0) return { ok: false, error: "Invalid id" };
  return { ok: true, id: n };
}

function parseStringMax(v: unknown, max: number): string | undefined {
  if (v === undefined || v === null) return undefined;
  if (typeof v !== "string") return undefined;
  return v.slice(0, max);
}

function parseLabelIds(v: unknown): number[] | undefined {
  if (v === undefined) return undefined;
  if (!Array.isArray(v)) return undefined;
  const out: number[] = [];
  for (const x of v) {
    const n = Number(x);
    if (Number.isInteger(n) && n > 0) out.push(n);
  }
  return out;
}

interface NoteInput {
  title?: string;
  content?: string;
  color?: string;
  isPinned?: boolean;
  isArchived?: boolean;
  labelIds?: number[];
}

function parseNoteInput(body: any, isCreate: boolean): { ok: true; data: NoteInput } | { ok: false; error: string } {
  if (!body || typeof body !== "object") return { ok: false, error: "Invalid body" };
  const data: NoteInput = {};
  if (body.title !== undefined) {
    const t = parseStringMax(body.title, 300);
    if (t === undefined) return { ok: false, error: "Invalid title" };
    data.title = t;
  } else if (isCreate) data.title = "";
  if (body.content !== undefined) {
    if (typeof body.content !== "string") return { ok: false, error: "Invalid content" };
    data.content = body.content;
  } else if (isCreate) data.content = "";
  if (body.color !== undefined) {
    const c = parseStringMax(body.color, 32);
    if (c === undefined) return { ok: false, error: "Invalid color" };
    data.color = c;
  } else if (isCreate) data.color = "default";
  if (body.isPinned !== undefined) {
    if (typeof body.isPinned !== "boolean") return { ok: false, error: "Invalid isPinned" };
    data.isPinned = body.isPinned;
  } else if (isCreate) data.isPinned = false;
  if (body.isArchived !== undefined) {
    if (typeof body.isArchived !== "boolean") return { ok: false, error: "Invalid isArchived" };
    data.isArchived = body.isArchived;
  }
  if (body.labelIds !== undefined) {
    const ids = parseLabelIds(body.labelIds);
    if (ids === undefined) return { ok: false, error: "Invalid labelIds" };
    data.labelIds = ids;
  } else if (isCreate) data.labelIds = [];
  return { ok: true, data };
}

function parseLabelName(body: any): { ok: true; name: string } | { ok: false; error: string } {
  if (!body || typeof body !== "object") return { ok: false, error: "Invalid body" };
  if (typeof body.name !== "string") return { ok: false, error: "Invalid name" };
  const name = body.name.trim();
  if (name.length === 0 || name.length > 80) return { ok: false, error: "Invalid name length" };
  return { ok: true, name };
}

async function loadNotesWithLabels(userId: number, archived: boolean) {
  const notes = await db
    .select()
    .from(notesTable)
    .where(and(eq(notesTable.userId, userId), eq(notesTable.isArchived, archived)))
    .orderBy(desc(notesTable.isPinned), desc(notesTable.updatedAt));

  if (notes.length === 0) return [];

  const noteIds = notes.map((n) => n.id);
  const assignments = await db
    .select()
    .from(noteLabelAssignmentsTable)
    .where(inArray(noteLabelAssignmentsTable.noteId, noteIds));

  const labelsByNote = new Map<number, number[]>();
  for (const a of assignments) {
    const arr = labelsByNote.get(a.noteId) ?? [];
    arr.push(a.labelId);
    labelsByNote.set(a.noteId, arr);
  }

  return notes.map((n) => ({ ...n, labelIds: labelsByNote.get(n.id) ?? [] }));
}

async function setNoteLabels(noteId: number, userId: number, labelIds: number[]) {
  await db.delete(noteLabelAssignmentsTable).where(eq(noteLabelAssignmentsTable.noteId, noteId));
  if (labelIds.length === 0) return;
  const valid = await db
    .select({ id: noteLabelsTable.id })
    .from(noteLabelsTable)
    .where(and(eq(noteLabelsTable.userId, userId), inArray(noteLabelsTable.id, labelIds)));
  if (valid.length === 0) return;
  await db.insert(noteLabelAssignmentsTable).values(
    valid.map((l) => ({ noteId, labelId: l.id })),
  );
}

router.get("/notes", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const archived = req.query.archived === "true";
  const notes = await loadNotesWithLabels(userId, archived);
  res.json(notes);
});

router.post("/notes", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const parsed = parseNoteInput(req.body, true);
  if (!parsed.ok) {
    res.status(400).json({ error: parsed.error });
    return;
  }
  const { labelIds = [], ...data } = parsed.data;
  const [note] = await db
    .insert(notesTable)
    .values({
      userId,
      title: data.title ?? "",
      content: data.content ?? "",
      color: data.color ?? "default",
      isPinned: data.isPinned ?? false,
      isArchived: data.isArchived ?? false,
    })
    .returning();
  if (labelIds.length > 0) await setNoteLabels(note.id, userId, labelIds);
  const [withLabels] = await loadOne(note.id, userId);
  res.status(201).json(withLabels);
});

router.patch("/notes/:id", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const id = parseIdParam(req.params.id);
  if (!id.ok) {
    res.status(400).json({ error: id.error });
    return;
  }
  const parsed = parseNoteInput(req.body, false);
  if (!parsed.ok) {
    res.status(400).json({ error: parsed.error });
    return;
  }
  const { labelIds, ...data } = parsed.data;
  const [existing] = await db
    .select()
    .from(notesTable)
    .where(and(eq(notesTable.id, id.id), eq(notesTable.userId, userId)));
  if (!existing) {
    res.status(404).json({ error: "Note not found" });
    return;
  }
  if (Object.keys(data).length > 0) {
    await db.update(notesTable).set(data).where(eq(notesTable.id, existing.id));
  }
  if (labelIds) await setNoteLabels(existing.id, userId, labelIds);
  const [withLabels] = await loadOne(existing.id, userId);
  res.json(withLabels);
});

async function loadOne(noteId: number, userId: number) {
  const [note] = await db
    .select()
    .from(notesTable)
    .where(and(eq(notesTable.id, noteId), eq(notesTable.userId, userId)));
  if (!note) return [null];
  const assignments = await db
    .select()
    .from(noteLabelAssignmentsTable)
    .where(eq(noteLabelAssignmentsTable.noteId, noteId));
  return [{ ...note, labelIds: assignments.map((a) => a.labelId) }];
}

router.delete("/notes/:id", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const id = parseIdParam(req.params.id);
  if (!id.ok) {
    res.status(400).json({ error: id.error });
    return;
  }
  const result = await db
    .delete(notesTable)
    .where(and(eq(notesTable.id, id.id), eq(notesTable.userId, userId)))
    .returning();
  if (result.length === 0) {
    res.status(404).json({ error: "Note not found" });
    return;
  }
  res.json({ success: true });
});

router.get("/note-labels", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const labels = await db
    .select()
    .from(noteLabelsTable)
    .where(eq(noteLabelsTable.userId, userId))
    .orderBy(noteLabelsTable.name);
  res.json(labels);
});

router.post("/note-labels", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const parsed = parseLabelName(req.body);
  if (!parsed.ok) {
    res.status(400).json({ error: parsed.error });
    return;
  }
  try {
    const [label] = await db
      .insert(noteLabelsTable)
      .values({ userId, name: parsed.name })
      .returning();
    res.status(201).json(label);
  } catch (e) {
    res.status(409).json({ error: "Label already exists" });
  }
});

router.patch("/note-labels/:id", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const id = parseIdParam(req.params.id);
  if (!id.ok) {
    res.status(400).json({ error: id.error });
    return;
  }
  const parsed = parseLabelName(req.body);
  if (!parsed.ok) {
    res.status(400).json({ error: parsed.error });
    return;
  }
  try {
    const [label] = await db
      .update(noteLabelsTable)
      .set({ name: parsed.name })
      .where(and(eq(noteLabelsTable.id, id.id), eq(noteLabelsTable.userId, userId)))
      .returning();
    if (!label) {
      res.status(404).json({ error: "Label not found" });
      return;
    }
    res.json(label);
  } catch (e) {
    res.status(409).json({ error: "Label already exists" });
  }
});

router.delete("/note-labels/:id", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const id = parseIdParam(req.params.id);
  if (!id.ok) {
    res.status(400).json({ error: id.error });
    return;
  }
  const result = await db
    .delete(noteLabelsTable)
    .where(and(eq(noteLabelsTable.id, id.id), eq(noteLabelsTable.userId, userId)))
    .returning();
  if (result.length === 0) {
    res.status(404).json({ error: "Label not found" });
    return;
  }
  res.json({ success: true });
});

export default router;
