import {
  UNLOCK_HEADER,
  unlockCookieName,
  type PublicResolution,
  type TDashboardPublicLinkServices,
} from "@/internal/dashboard/dashboard-public-link.services";
import {
  relativePathFromServeUrl,
  serveDashboardAsset,
} from "@/internal/dashboard/dashboard-serve";
import { getBaseCookieConfig } from "@/lib/cookie";
import { logError } from "@/lib/logger";
import type { TDashboardStorage } from "@/lib/storage/dashboard-storage";
import type { Context } from "hono";
import { getCookie, setCookie } from "hono/cookie";

type TDashboardPublicControllerDeps = {
  DashboardPublicLinkServices: TDashboardPublicLinkServices;
  DashboardStorage: TDashboardStorage;
};

/** Two hours, matching the unlock token's own expiry. */
const UNLOCK_COOKIE_MAX_AGE = 2 * 60 * 60;

export const DashboardPublicController = (
  ctx: Context,
  { DashboardPublicLinkServices, DashboardStorage }: TDashboardPublicControllerDeps,
) => {
  const clientIp = () =>
    ctx.req.header("x-forwarded-for")?.split(",")[0]?.trim() ??
    ctx.req.header("x-real-ip") ??
    null;

  /**
   * The ONE response every non-serviceable link gets: unknown token, revoked,
   * expired, dashboard deleted, dashboard still a draft, no published version.
   * Same status, same body, every time — a prober learns nothing about which
   * tokens exist.
   */
  const unavailable = () =>
    ctx.json(
      { success: false, message: "This link is unavailable.", data: null },
      404,
    );

  const readUnlockToken = (linkId: string) =>
    getCookie(ctx, unlockCookieName(linkId)) ??
    ctx.req.header(UNLOCK_HEADER) ??
    undefined;

  const Serve = async () => {
    try {
      const token = ctx.req.param("token") ?? "";

      // Resolve once WITHOUT an unlock token to learn the link id, then re-check
      // with the cookie that is scoped to that id. (The cookie name embeds the
      // link id, so we can't read it before resolving.)
      const first = await DashboardPublicLinkServices.ResolvePublicToken(token);
      if (first.state === "unavailable") return unavailable();

      const resolution: PublicResolution =
        first.state === "password_required"
          ? await DashboardPublicLinkServices.ResolvePublicToken(
              token,
              readUnlockToken(first.linkId),
            )
          : first;

      if (resolution.state === "unavailable") return unavailable();
      if (resolution.state === "password_required") {
        // The only distinction this design accepts: a password prompt
        // inherently reveals that the link resolved.
        return ctx.json(
          {
            success: false,
            message: "This link requires a password.",
            data: { code: "PASSWORD_REQUIRED" },
          },
          401,
        );
      }

      const { link, dashboard, version } = resolution;

      const decoded = relativePathFromServeUrl(ctx.req.path, `/${token}/serve`);
      if (decoded === null) return unavailable();

      // Note what is NOT here: no ?v= handling. A public viewer always gets the
      // dashboard's current published version; version preview stays admin-only.
      const outcome = await serveDashboardAsset({
        storage: DashboardStorage,
        dashboardId: dashboard.id,
        version,
        requestedPath: decoded,
        viewer: { consoleUserId: null, publicLinkId: link.id },
        ipAddress: clientIp(),
        recordView: (entry) =>
          DashboardPublicLinkServices.RecordPublicView({
            dashboardId: entry.dashboardId,
            versionId: entry.versionId,
            publicLinkId: link.id,
            ipAddress: entry.ipAddress,
          }),
        // Entry point only — one bump per dashboard load, not per asset.
        onEntryPointServed: () =>
          DashboardPublicLinkServices.IncrementViewCount(link.id),
      });

      // A missing asset inside a valid bundle is a genuine 404, but we still
      // answer with the generic shell so the public surface has one error shape.
      if (!outcome.ok) return unavailable();
      return outcome.response;
    } catch (err) {
      logError(err);
      return unavailable();
    }
  };

  const Unlock = async () => {
    try {
      const token = ctx.req.param("token") ?? "";
      const body = (await ctx.req
        .json<{ password?: string }>()
        .catch(() => ({}))) as { password?: string };
      const password = typeof body.password === "string" ? body.password : "";

      const result = await DashboardPublicLinkServices.AttemptUnlock(token, password);
      if (!result.ok) {
        // Unknown / revoked / expired / no-password links all get the generic
        // shell; only a genuine password mismatch on a real password-protected
        // link answers 401, which reveals nothing the prompt didn't already.
        if (result.reason === "unavailable") return unavailable();
        return ctx.json(
          { success: false, message: "Incorrect password.", data: null },
          401,
        );
      }

      // HttpOnly so page scripts (including the bundle's own) can never read it.
      setCookie(ctx, unlockCookieName(result.linkId), result.token, {
        ...getBaseCookieConfig(),
        maxAge: UNLOCK_COOKIE_MAX_AGE,
      });

      return ctx.json({
        success: true,
        message: "Unlocked",
        // Returned for non-cookie clients; carries no secret beyond link scope.
        data: { unlockToken: result.token, expiresIn: UNLOCK_COOKIE_MAX_AGE },
      });
    } catch (err) {
      logError(err);
      return unavailable();
    }
  };

  return { Serve, Unlock };
};
