import { V1Controllers } from "@/cmd/routes/v1/controllers";
import {
  CreateDashboardSchema,
  CreatePublicLinkSchema,
  SetEntryPointSchema,
  PublishDashboardSchema,
  RollbackDashboardSchema,
  SetDashboardAccessSchema,
  UpdateDashboardSchema,
} from "@/cmd/routes/v1/controllers/dashboard.schema";
import { sanitizePayload } from "@/internal/middlewares/sanitzePayload";
import { withConsoleUserRole } from "@/middlewares/console-role";
import { withConsoleUserSession } from "@/middlewares/withConsoleUserSession";
import { error } from "@/lib/response";
import { Hono } from "hono";
import { bodyLimit } from "hono/body-limit";

export const DashboardRoutes = new Hono();

/**
 * Hard cap on the raw upload body, enforced at the edge BEFORE anything is
 * buffered or parsed.
 *
 * 60 MB against the Bundle Contract's 50 MB *unzipped* cap: a zip of a
 * 50 MB bundle is normally far smaller, and the 10 MB of headroom covers the
 * multipart envelope plus the pathological case of an already-compressed
 * payload that the zip can't shrink. Oversized requests are refused with 413
 * here — they never reach the validator, so a 2 GB POST costs us nothing.
 */
const MAX_UPLOAD_BYTES = 60 * 1024 * 1024; // 62,914,560

const uploadBodyLimit = bodyLimit({
  maxSize: MAX_UPLOAD_BYTES,
  onError: (c) =>
    error(
      c,
      `Upload exceeds the maximum size of ${Math.floor(MAX_UPLOAD_BYTES / (1024 * 1024))} MB.`,
      413,
    ),
});

// ---------------------------------------------------------------------------
// Serving proxy.
//
// Registered FIRST so "/:id/serve/*" wins over "/:id". It carries only
// withConsoleUserSession(): per-dashboard visibility is decided in the service
// via canUserViewDashboard, not by a feature privilege — a user granted one
// dashboard has no dashboard:read role at all, and must still be able to open
// the thing they were granted.
//
// (Prompt 4 adds the public-token path alongside this; the seam is the
// requestedVersionId/consoleUserId resolution inside ResolveServeTarget.)
// ---------------------------------------------------------------------------
DashboardRoutes.get("/:id/serve", withConsoleUserSession(), async (ctx) =>
  V1Controllers(ctx).DashboardController.Serve(),
);
DashboardRoutes.get("/:id/serve/*", withConsoleUserSession(), async (ctx) =>
  V1Controllers(ctx).DashboardController.Serve(),
);

// ---------------------------------------------------------------------------
// Admin surface
// ---------------------------------------------------------------------------

DashboardRoutes.post(
  "/",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "create"),
  sanitizePayload(CreateDashboardSchema),
  async (ctx) => V1Controllers(ctx).DashboardController.Create(),
);

DashboardRoutes.get(
  "/",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "read"),
  async (ctx) => V1Controllers(ctx).DashboardController.List(),
);

// Version routes are declared before "/:id" so the more specific paths match.
DashboardRoutes.post(
  "/:id/versions",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "update"),
  uploadBodyLimit,
  async (ctx) => V1Controllers(ctx).DashboardController.UploadVersion(),
);

DashboardRoutes.get(
  "/:id/versions",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "read"),
  async (ctx) => V1Controllers(ctx).DashboardController.ListVersions(),
);

DashboardRoutes.post(
  "/:id/publish",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "update"),
  sanitizePayload(PublishDashboardSchema),
  async (ctx) => V1Controllers(ctx).DashboardController.Publish(),
);

DashboardRoutes.post(
  "/:id/rollback",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "update"),
  sanitizePayload(RollbackDashboardSchema),
  async (ctx) => V1Controllers(ctx).DashboardController.Rollback(),
);

DashboardRoutes.put(
  "/:id/access",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "update"),
  sanitizePayload(SetDashboardAccessSchema),
  async (ctx) => V1Controllers(ctx).DashboardController.SetAccess(),
);

DashboardRoutes.get(
  "/:id/access",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "read"),
  async (ctx) => V1Controllers(ctx).DashboardController.GetAccess(),
);

DashboardRoutes.get(
  "/:id/audit",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "read"),
  async (ctx) => V1Controllers(ctx).DashboardController.GetAudit(),
);

/**
 * Cover image. 5 MB is generous for a card thumbnail and two orders of
 * magnitude below the bundle cap, so an oversized image is refused at the edge.
 */
const MAX_COVER_BYTES = 5 * 1024 * 1024;

DashboardRoutes.post(
  "/:id/cover",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "update"),
  bodyLimit({
    maxSize: MAX_COVER_BYTES,
    onError: (c) => error(c, "Cover exceeds the maximum size of 5 MB.", 413),
  }),
  async (ctx) => V1Controllers(ctx).DashboardController.UploadCover(),
);

/*
 * Session-only, like the serve route: a user granted a single dashboard has no
 * dashboard:read privilege, but must still see that dashboard's cover in the
 * listing. Per-dashboard access is enforced in the service.
 */
DashboardRoutes.get("/:id/cover", withConsoleUserSession(), async (ctx) =>
  V1Controllers(ctx).DashboardController.ServeCover(),
);

// Draft-only entry-point override; the service refuses once published.
DashboardRoutes.patch(
  "/:id/versions/:versionId/entry-point",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "update"),
  sanitizePayload(SetEntryPointSchema),
  async (ctx) => V1Controllers(ctx).DashboardController.SetEntryPoint(),
);

// --- Public share links (admin side). The public viewer endpoints are
// session-free and mounted separately under /v1/public/dashboards.
DashboardRoutes.get(
  "/:id/public-links",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "read"),
  async (ctx) => V1Controllers(ctx).DashboardController.ListPublicLinks(),
);
DashboardRoutes.post(
  "/:id/public-links",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "update"),
  sanitizePayload(CreatePublicLinkSchema),
  async (ctx) => V1Controllers(ctx).DashboardController.CreatePublicLink(),
);
DashboardRoutes.delete(
  "/:id/public-links/:linkId",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "update"),
  async (ctx) => V1Controllers(ctx).DashboardController.RevokePublicLink(),
);

DashboardRoutes.get(
  "/:id",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "read"),
  async (ctx) => V1Controllers(ctx).DashboardController.Detail(),
);

DashboardRoutes.patch(
  "/:id",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "update"),
  sanitizePayload(UpdateDashboardSchema),
  async (ctx) => V1Controllers(ctx).DashboardController.Update(),
);

DashboardRoutes.delete(
  "/:id",
  withConsoleUserSession(),
  withConsoleUserRole("dashboard", "delete"),
  async (ctx) => V1Controllers(ctx).DashboardController.Delete(),
);
