import type { TDashboardPublicLinkServices } from "@/internal/dashboard/dashboard-public-link.services";
import type { TDashboardServices } from "@/internal/dashboard/dashboard.services";
import type { ServiceResult } from "@/internal/dashboard/dashboard.services";
import {
  relativePathFromServeUrl,
  serveDashboardAsset,
} from "@/internal/dashboard/dashboard-serve";
import type { TDashboardStorage } from "@/lib/storage/dashboard-storage";
import { logError } from "@/lib/logger";
import { error, success } from "@/lib/response";
import type { Context } from "hono";
import { Readable } from "node:stream";
import type z from "zod";
import type {
  CreateDashboardSchema,
  CreatePublicLinkSchema,
  SetEntryPointSchema,
  PublishDashboardSchema,
  RollbackDashboardSchema,
  SetDashboardAccessSchema,
  UpdateDashboardSchema,
} from "./dashboard.schema";

type TDashboardControllerDeps = {
  DashboardServices: TDashboardServices;
  DashboardPublicLinkServices: TDashboardPublicLinkServices;
  DashboardStorage: TDashboardStorage;
};

const COVER_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp"]);

const ZIP_CONTENT_TYPES = new Set([
  "application/zip",
  "application/x-zip-compressed",
  "application/x-zip",
  "multipart/x-zip",
  // Some browsers/clients send this for a .zip; the extension check below and
  // the validator's own zip parsing are the real gates.
  "application/octet-stream",
]);

export const DashboardController = (
  ctx: Context,
  {
    DashboardServices,
    DashboardPublicLinkServices,
    DashboardStorage,
  }: TDashboardControllerDeps,
) => {
  const actor = () => ({
    userId: (ctx.get("consoleUserId") as string | undefined) ?? "",
    email: (ctx.get("adminEmail") as string | undefined) ?? null,
  });

  const clientIp = () =>
    ctx.req.header("x-forwarded-for")?.split(",")[0]?.trim() ??
    ctx.req.header("x-real-ip") ??
    null;

  /** Map a service result onto the response envelope. */
  const respond = <T>(result: ServiceResult<T>, message?: string) => {
    if (!result.ok) {
      return error(ctx, result.message, result.status, result.errors);
    }
    return success(ctx, result.data, message);
  };

  // -------------------------------------------------------------------------
  // CRUD
  // -------------------------------------------------------------------------

  const Create = async () => {
    try {
      const raw: z.infer<typeof CreateDashboardSchema> = await ctx.req.json();
      const result = await DashboardServices.CreateDashboard({
        name: raw.name,
        description: raw.description ?? null,
        actor: actor(),
      });
      if (result.ok) {
        ctx.set("entityId", (result.data as { id: string }).id);
      }
      return respond(result, "Dashboard created");
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const List = async () => {
    try {
      const { userId } = actor();
      return respond(await DashboardServices.ListDashboardsForUser(userId));
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const Detail = async () => {
    try {
      const { id } = ctx.req.param();
      const { userId } = actor();
      return respond(await DashboardServices.GetDashboardDetail(id, userId));
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const Update = async () => {
    try {
      const { id } = ctx.req.param();
      ctx.set("entityId", id);
      const raw: z.infer<typeof UpdateDashboardSchema> = await ctx.req.json();
      return respond(
        await DashboardServices.UpdateMeta({
          dashboardId: id,
          patch: raw,
          actor: actor(),
        }),
        "Dashboard updated",
      );
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const Delete = async () => {
    try {
      const { id } = ctx.req.param();
      ctx.set("entityId", id);
      return respond(
        await DashboardServices.DeleteDashboard({ dashboardId: id, actor: actor() }),
        "Dashboard deleted",
      );
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  // -------------------------------------------------------------------------
  // Versions
  // -------------------------------------------------------------------------

  const UploadVersion = async () => {
    try {
      const { id } = ctx.req.param();
      ctx.set("entityId", id);

      const contentType = ctx.req.header("content-type") ?? "";
      if (!contentType.toLowerCase().includes("multipart/form-data")) {
        return error(ctx, "Upload must be multipart/form-data.", 415);
      }

      const body = await ctx.req.parseBody();
      const bundle = body["bundle"];
      if (!(bundle instanceof File)) {
        return error(ctx, "Expected a zip file in the 'bundle' field.", 400);
      }

      const looksLikeZip =
        bundle.name.toLowerCase().endsWith(".zip") ||
        ZIP_CONTENT_TYPES.has((bundle.type || "").toLowerCase());
      if (!looksLikeZip) {
        return error(ctx, "The 'bundle' field must be a .zip archive.", 415);
      }

      // Multipart fields arrive as strings.
      const publish = body["publish"] === "true";

      const zipBuffer = Buffer.from(await bundle.arrayBuffer());
      const result = await DashboardServices.UploadVersion({
        dashboardId: id,
        zipBuffer,
        actor: actor(),
        publish,
      });
      return respond(result, "Version uploaded");
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const ListVersions = async () => {
    try {
      const { id } = ctx.req.param();
      const { userId } = actor();
      return respond(await DashboardServices.ListVersions(id, userId));
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const Publish = async () => {
    try {
      const { id } = ctx.req.param();
      ctx.set("entityId", id);
      const raw: z.infer<typeof PublishDashboardSchema> = await ctx.req.json();
      return respond(
        await DashboardServices.PublishDashboard({
          dashboardId: id,
          versionId: raw.versionId,
          actor: actor(),
        }),
        "Dashboard published",
      );
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const Rollback = async () => {
    try {
      const { id } = ctx.req.param();
      ctx.set("entityId", id);
      const raw: z.infer<typeof RollbackDashboardSchema> = await ctx.req.json();
      return respond(
        await DashboardServices.RollbackDashboard({
          dashboardId: id,
          versionId: raw.versionId,
          actor: actor(),
        }),
        "Dashboard rolled back",
      );
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  // -------------------------------------------------------------------------
  // Access list + audit
  // -------------------------------------------------------------------------

  const GetAccess = async () => {
    try {
      const { id } = ctx.req.param();
      return respond(await DashboardServices.GetAccessList(id, actor()));
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const SetAccess = async () => {
    try {
      const { id } = ctx.req.param();
      ctx.set("entityId", id);
      const raw: z.infer<typeof SetDashboardAccessSchema> = await ctx.req.json();
      return respond(
        await DashboardServices.SetAccessList({
          dashboardId: id,
          userIds: raw.userIds,
          actor: actor(),
        }),
        "Access list updated",
      );
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const GetAudit = async () => {
    try {
      const { id } = ctx.req.param();
      return respond(await DashboardServices.GetAuditLog(id, actor()));
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  // -------------------------------------------------------------------------
  // Cover image
  // -------------------------------------------------------------------------

  const UploadCover = async () => {
    try {
      const { id } = ctx.req.param();
      ctx.set("entityId", id);

      const contentType = ctx.req.header("content-type") ?? "";
      if (!contentType.toLowerCase().includes("multipart/form-data")) {
        return error(ctx, "Upload must be multipart/form-data.", 415);
      }
      const body = await ctx.req.parseBody();
      const image = body["image"];
      if (!(image instanceof File)) {
        return error(ctx, "Expected an image in the 'image' field.", 400);
      }
      const ext = image.name.slice(image.name.lastIndexOf(".") + 1).toLowerCase();
      if (!COVER_EXTENSIONS.has(ext)) {
        return error(ctx, `Cover must be one of: ${[...COVER_EXTENSIONS].join(", ")}.`, 415);
      }

      return respond(
        await DashboardServices.UploadCover({
          dashboardId: id,
          fileName: image.name,
          contentType: image.type,
          buffer: Buffer.from(await image.arrayBuffer()),
          actor: actor(),
        }),
        "Cover updated",
      );
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  /**
   * Streams the cover for an <img>. Deliberately NOT the bundle CSP — this is a
   * plain image, not a sandboxed document — but it is access-scoped through the
   * same canUserViewDashboard helper, and a dashboard you can't see 404s rather
   * than 403s so covers can't be used to enumerate dashboards.
   */
  const ServeCover = async () => {
    try {
      const { id } = ctx.req.param();
      const { userId } = actor();
      const resolved = await DashboardServices.ResolveCover(id, userId);
      if (!resolved.ok) return error(ctx, resolved.message, resolved.status);

      const object = await DashboardStorage.getObject(resolved.data.path);
      if (!object) return error(ctx, "Cover not found.", 404);

      const headers = new Headers();
      headers.set("Content-Type", object.contentType);
      if (object.size > 0) headers.set("Content-Length", String(object.size));
      headers.set("Cache-Control", "private, max-age=300");
      headers.set("X-Content-Type-Options", "nosniff");
      return new Response(
        Readable.toWeb(object.stream) as unknown as ReadableStream,
        { status: 200, headers },
      );
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const SetEntryPoint = async () => {
    try {
      const { id, versionId } = ctx.req.param();
      ctx.set("entityId", id);
      const raw: z.infer<typeof SetEntryPointSchema> = await ctx.req.json();
      return respond(
        await DashboardServices.SetVersionEntryPoint({
          dashboardId: id,
          versionId,
          entryPoint: raw.entryPoint,
          actor: actor(),
        }),
        "Entry point updated",
      );
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  // -------------------------------------------------------------------------
  // Serving proxy
  // -------------------------------------------------------------------------

  const Serve = async () => {
    try {
      const { id } = ctx.req.param();
      const { userId } = actor();

      // ?v= is an ADMIN-ONLY preview affordance; ResolveServeTarget enforces
      // that. The public path never passes it at all.
      const requestedVersionId = ctx.req.query("v") ?? null;
      const target = await DashboardServices.ResolveServeTarget({
        dashboardId: id,
        userId,
        requestedVersionId,
      });
      if (!target.ok) {
        return error(ctx, target.message, target.status);
      }
      const { dashboard, version } = target.data;

      const decoded = relativePathFromServeUrl(ctx.req.path, `/${id}/serve`);
      if (decoded === null) {
        return error(ctx, "Malformed asset path.", 400);
      }

      // ONE serving core, shared with the public share-link path.
      const outcome = await serveDashboardAsset({
        storage: DashboardStorage,
        dashboardId: dashboard.id,
        version,
        requestedPath: decoded,
        viewer: { consoleUserId: userId || null, publicLinkId: null },
        ipAddress: clientIp(),
        recordView: DashboardServices.RecordView,
      });
      if (!outcome.ok) {
        return error(ctx, outcome.message, outcome.status);
      }
      return outcome.response;
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  // -------------------------------------------------------------------------
  // Public share links (admin side). isDashboardAdmin is asserted inside the
  // service too, not just by the route guard.
  // -------------------------------------------------------------------------

  const CreatePublicLink = async () => {
    try {
      const { id } = ctx.req.param();
      ctx.set("entityId", id);
      const raw: z.infer<typeof CreatePublicLinkSchema> = await ctx.req.json();
      // The response carries the raw token exactly once; it is never logged.
      return respond(
        await DashboardPublicLinkServices.CreateLink({
          dashboardId: id,
          expiresAt: raw.expiresAt ?? null,
          password: raw.password ?? null,
          actor: actor(),
        }),
        "Share link created — copy it now, it will not be shown again",
      );
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const ListPublicLinks = async () => {
    try {
      const { id } = ctx.req.param();
      return respond(await DashboardPublicLinkServices.ListLinks(id, actor()));
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  const RevokePublicLink = async () => {
    try {
      const { id, linkId } = ctx.req.param();
      ctx.set("entityId", id);
      return respond(
        await DashboardPublicLinkServices.RevokeLink({
          dashboardId: id,
          linkId,
          actor: actor(),
        }),
        "Share link revoked",
      );
    } catch (err) {
      logError(err);
      return error(ctx, undefined, undefined, err);
    }
  };

  return {
    Create,
    List,
    Detail,
    Update,
    Delete,
    UploadVersion,
    ListVersions,
    Publish,
    Rollback,
    GetAccess,
    SetAccess,
    GetAudit,
    Serve,
    UploadCover,
    ServeCover,
    SetEntryPoint,
    CreatePublicLink,
    ListPublicLinks,
    RevokePublicLink,
  };
};
