import type { FastifyInstance, FastifyRequest } from "fastify";
import type { MultipartFile } from "@fastify/multipart";
import { z } from "zod";

import { env } from "../config.js";
import { AppError } from "../lib/errors.js";
import { supabaseAdmin } from "../lib/supabase.js";
import { isoNow } from "../lib/utils.js";
import { requireOrganizationMembership, requireUser } from "../services/auth-service.js";
import { logCampaignActivity } from "../services/activity-log-service.js";
import { cancelCampaignSend, cloneCampaign, type CampaignBlock, loadCampaign, persistCampaignContent, queueCampaignSend, scheduleCampaignSend, sendCampaignTest } from "../services/campaign-service.js";
import { ResendProvider } from "../services/email/resend-provider.js";

const provider = new ResendProvider();

const createCampaignSchema = z.object({
  organizationId: z.string().uuid(),
  name: z.string().trim().min(2),
  subject: z.string().min(2),
  fromName: z.string().min(2),
  fromEmail: z.string().email(),
  editorMode: z.enum(["builder", "html"]).default("builder")
});
const updateCampaignSchema = z.object({
  name: z.string().trim().min(2),
  subject: z.string().min(2),
  fromName: z.string().min(2),
  fromEmail: z.string().email()
});

const updateContentSchema = z.object({
  editorMode: z.enum(["builder", "html"]),
  htmlBody: z.string().optional(),
  textBody: z.string().optional(),
  blocks: z
    .array(
      z.object({
        id: z.string().optional().default(""),
        type: z.enum(["hero", "text", "image", "button", "divider", "spacer", "social", "footer", "header_logo", "columns", "quote", "video", "countdown", "list"]),
        content: z.record(z.string(), z.any())
      })
    )
    .optional()
});

const sendCampaignSchema = z.object({
  contactIds: z.array(z.string().uuid()).optional()
});
const scheduleCampaignSchema = z.object({
  scheduledFor: z.string().datetime(),
  contactIds: z.array(z.string().uuid()).optional()
});
const cloneCampaignSchema = z.object({
  name: z.string().trim().min(2).max(160).optional(),
  asReusableTemplate: z.boolean().optional().default(false)
});
const createVersionSchema = z.object({
  source: z.enum(["manual", "autosave", "restore", "publish"]).optional().default("manual")
});
const deleteAssetSchema = z.object({
  assetId: z.string().min(1).optional(),
  assetPath: z.string().min(1).optional(),
});

async function loadCampaignVersionBlocks(campaignId: string) {
  const { data, error } = await supabaseAdmin
    .from("campaign_blocks")
    .select("id, block_type, position, content")
    .eq("campaign_id", campaignId)
    .order("position", { ascending: true });

  if (error) {
    throw new AppError(400, "CAMPAIGN_VERSION_BLOCKS_FAILED", error.message);
  }

  return ((data ?? []) as Array<Record<string, any>>).map((block) => ({
    id: block.id,
    type: block.block_type,
    content: block.content ?? {},
  }));
}

async function createCampaignVersionSnapshot(campaignId: string, actorAuthUserId?: string | null, source: "manual" | "autosave" | "restore" | "publish" = "manual") {
  const campaign = await loadCampaign(campaignId);
  const blocks = await loadCampaignVersionBlocks(campaignId);

  const { data, error } = await supabaseAdmin
    .from("campaign_versions")
    .insert({
      organization_id: campaign.organization_id,
      campaign_id: campaign.id,
      created_by_auth_user_id: actorAuthUserId ?? null,
      name: campaign.name ?? "Campaign draft",
      subject: campaign.subject ?? "",
      from_name: campaign.from_name ?? "",
      from_email: campaign.from_email ?? "",
      editor_mode: campaign.editor_mode ?? "builder",
      html_body: campaign.html_body ?? null,
      text_body: campaign.text_body ?? null,
      blocks,
      save_source: source,
      created_at: isoNow(),
    })
    .select("id, campaign_id, name, subject, from_name, from_email, editor_mode, save_source, created_at")
    .single();

  if (error || !data) {
    throw new AppError(400, "CAMPAIGN_VERSION_CREATE_FAILED", error?.message ?? "Unable to create campaign version.");
  }

  const { data: staleVersions } = await supabaseAdmin
    .from("campaign_versions")
    .select("id")
    .eq("campaign_id", campaignId)
    .order("created_at", { ascending: false })
    .range(5, 99);

  const staleIds = ((staleVersions ?? []) as Array<Record<string, any>>)
    .map((row) => String(row.id ?? ""))
    .filter(Boolean);

  if (staleIds.length) {
    await supabaseAdmin.from("campaign_versions").delete().in("id", staleIds);
  }

  return data;
}

function isMissingColumnError(error: { message?: string | null } | null | undefined, columnName: string) {
  const message = String(error?.message ?? "").toLowerCase();
  return message.includes(columnName.toLowerCase()) && (
    message.includes("column")
    || message.includes("schema cache")
    || message.includes("does not exist")
  );
}

function normalizeCampaignTitle(value: string) {
  return value.trim().replace(/\s+/g, " ").toLowerCase();
}

async function assertCampaignTitleAvailable(input: {
  organizationId: string;
  actorAuthUserId: string;
  name: string;
  excludeCampaignId?: string;
}) {
  const normalizedTitle = normalizeCampaignTitle(input.name);

  const { data, error } = await supabaseAdmin
    .from("campaigns")
    .select("id, name")
    .eq("organization_id", input.organizationId)
    .eq("created_by_auth_user_id", input.actorAuthUserId);

  if (error) {
    throw new AppError(400, "CAMPAIGN_TITLE_LOOKUP_FAILED", error.message);
  }

  const duplicate = (data ?? []).find((campaign: any) => {
    const campaignId = String(campaign.id ?? "");
    if (input.excludeCampaignId && campaignId === input.excludeCampaignId) return false;
    return normalizeCampaignTitle(String(campaign.name ?? "")) === normalizedTitle;
  });

  if (duplicate) {
    throw new AppError(400, "CAMPAIGN_TITLE_DUPLICATE", "You already have a campaign with this title. Rename it before creating another one.");
  }
}

function resolvePublicOrigin(request: FastifyRequest) {
  const forwardedProtocol = request.headers["x-forwarded-proto"];
  const forwardedHost = request.headers["x-forwarded-host"];
  const protocol = Array.isArray(forwardedProtocol)
    ? forwardedProtocol[0]
    : String(forwardedProtocol ?? request.protocol ?? "https").split(",")[0]?.trim() || "https";
  const host = Array.isArray(forwardedHost)
    ? forwardedHost[0]
    : Array.isArray(request.headers.host)
      ? request.headers.host[0]
      : forwardedHost ?? request.headers.host;

  return host ? `${protocol}://${host}` : env.APP_BASE_URL.replace(/\/$/, "");
}

function buildCampaignAssetUrl(request: FastifyRequest, organizationId: string, assetPath: string) {
  return `${resolvePublicOrigin(request)}/api/public/campaign-assets/${organizationId}?path=${encodeURIComponent(assetPath)}`;
}

function toCampaignAssetResponse(request: FastifyRequest, organizationId: string, asset: Record<string, any>) {
  const assetPath = String(asset.storage_path ?? "");
  return {
    id: String(asset.id ?? assetPath),
    assetPath,
    assetUrl: buildCampaignAssetUrl(request, organizationId, assetPath),
    fileName: String(asset.file_name ?? asset.name ?? assetPath.split("/").pop() ?? "image"),
    fileSize: asset.file_size == null ? null : Number(asset.file_size),
    mimeType: asset.mime_type ? String(asset.mime_type) : null,
    altText: asset.alt_text ? String(asset.alt_text) : null,
    uploadedVia: asset.uploaded_via ? String(asset.uploaded_via) : "builder",
    createdAt: asset.created_at ? String(asset.created_at) : null,
    updatedAt: asset.updated_at ? String(asset.updated_at) : null,
    createdByAuthUserId: asset.created_by_auth_user_id ? String(asset.created_by_auth_user_id) : null,
    isIndexed: Boolean(asset.is_indexed ?? asset.id),
  };
}

async function toBuffer(file: MultipartFile) {
  const chunks: Buffer[] = [];

  for await (const chunk of file.file) {
    chunks.push(Buffer.from(chunk));
  }

  return Buffer.concat(chunks);
}

function normalizeFilename(filename?: string) {
  return String(filename ?? "asset")
    .replace(/[^a-zA-Z0-9._-]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-|-$/g, "") || "asset";
}

export function registerCampaignRoutes(app: FastifyInstance) {
  app.get("/api/public/campaign-assets/:organizationId", async (request, reply) => {
    const params = z.object({ organizationId: z.string().uuid() }).parse(request.params);
    const query = z.object({ path: z.string().min(1) }).parse(request.query);
    const expectedPrefix = `orgs/${params.organizationId}/campaign-assets/`;

    if (!query.path.startsWith(expectedPrefix)) {
      throw new AppError(403, "CAMPAIGN_ASSET_FORBIDDEN", "Asset path does not belong to this organization.");
    }

    const download = await supabaseAdmin.storage.from("brand-assets").download(query.path);

    if (download.error || !download.data) {
      throw new AppError(404, "CAMPAIGN_ASSET_NOT_FOUND", download.error?.message ?? "Asset not found.");
    }

    const buffer = Buffer.from(await download.data.arrayBuffer());
    if (download.data.type) {
      reply.header("Content-Type", download.data.type);
    }
    reply.header("Cache-Control", "public, max-age=3600");
    return reply.send(buffer);
  });

  app.post("/api/campaign-assets/:organizationId", async (request) => {
    const params = z.object({ organizationId: z.string().uuid() }).parse(request.params);
    const user = await requireUser(request);
    await requireOrganizationMembership(request, params.organizationId);

    const parts = request.parts();
    let filePart: MultipartFile | null = null;

    for await (const part of parts) {
      if (part.type === "file") {
        filePart = part;
        break;
      }
    }

    if (!filePart) {
      throw new AppError(400, "CAMPAIGN_ASSET_REQUIRED", "Select an image file to upload.");
    }

    const buffer = await toBuffer(filePart);
    const now = isoNow();
    const assetPath = `orgs/${params.organizationId}/campaign-assets/${Date.now()}-${normalizeFilename(filePart.filename)}`;
    const upload = await supabaseAdmin.storage.from("brand-assets").upload(assetPath, buffer, {
      contentType: filePart.mimetype,
      upsert: true
    });

    if (upload.error) {
      throw new AppError(400, "CAMPAIGN_ASSET_UPLOAD_FAILED", upload.error.message);
    }

    const { data: assetRecord, error: assetRecordError } = await supabaseAdmin
      .from("campaign_assets")
      .insert({
        organization_id: params.organizationId,
        created_by_auth_user_id: user.id,
        updated_by_auth_user_id: user.id,
        storage_path: assetPath,
        file_name: normalizeFilename(filePart.filename),
        file_size: buffer.byteLength,
        mime_type: filePart.mimetype ?? null,
        uploaded_via: "builder",
        created_at: now,
        updated_at: now,
      })
      .select("id, storage_path, file_name, file_size, mime_type, alt_text, uploaded_via, created_at, updated_at, created_by_auth_user_id")
      .single();

    if (assetRecordError || !assetRecord) {
      throw new AppError(400, "CAMPAIGN_ASSET_RECORD_FAILED", assetRecordError?.message ?? "Asset metadata could not be stored.");
    }

    return {
      assetPath,
      assetUrl: buildCampaignAssetUrl(request, params.organizationId, assetPath),
      asset: toCampaignAssetResponse(request, params.organizationId, assetRecord),
    };
  });

  app.get("/api/campaign-assets/:organizationId", async (request) => {
    const params = z.object({ organizationId: z.string().uuid() }).parse(request.params);
    await requireOrganizationMembership(request, params.organizationId);

    const prefix = `orgs/${params.organizationId}/campaign-assets`;
    const [storageResult, databaseResult] = await Promise.all([
      supabaseAdmin.storage.from("brand-assets").list(prefix, {
        limit: 500,
        sortBy: { column: "created_at", order: "desc" },
      }),
      supabaseAdmin
        .from("campaign_assets")
        .select("id, storage_path, file_name, file_size, mime_type, alt_text, uploaded_via, created_at, updated_at, created_by_auth_user_id")
        .eq("organization_id", params.organizationId)
        .order("created_at", { ascending: false }),
    ]);

    if (storageResult.error) {
      throw new AppError(400, "CAMPAIGN_ASSET_LIST_FAILED", storageResult.error.message);
    }

    if (databaseResult.error) {
      throw new AppError(400, "CAMPAIGN_ASSET_LIST_FAILED", databaseResult.error.message);
    }

    const databaseAssets = (databaseResult.data ?? []) as Array<Record<string, any>>;
    const assetsByPath = new Map<string, Record<string, any>>();

    databaseAssets.forEach((asset) => {
      const assetPath = String(asset.storage_path ?? "");
      if (assetPath) assetsByPath.set(assetPath, asset);
    });

    for (const item of storageResult.data ?? []) {
      const name = String(item.name ?? "");
      if (!name || name.endsWith("/")) continue;
      const assetPath = `${prefix}/${name}`;
      if (assetsByPath.has(assetPath)) continue;
      assetsByPath.set(assetPath, {
        id: `legacy:${assetPath}`,
        storage_path: assetPath,
        file_name: name,
        file_size: item.metadata?.size ?? null,
        mime_type: item.metadata?.mimetype ?? null,
        alt_text: null,
        uploaded_via: "legacy",
        created_at: item.created_at ?? item.updated_at ?? null,
        updated_at: item.updated_at ?? item.created_at ?? null,
        created_by_auth_user_id: null,
        is_indexed: false,
      });
    }

    const items = Array.from(assetsByPath.values())
      .sort((left, right) => {
        const leftTime = new Date(String(left.created_at ?? left.updated_at ?? 0)).getTime();
        const rightTime = new Date(String(right.created_at ?? right.updated_at ?? 0)).getTime();
        return rightTime - leftTime;
      })
      .map((asset) => toCampaignAssetResponse(request, params.organizationId, asset));

    return { items };
  });

  app.delete("/api/campaign-assets/:organizationId", async (request) => {
    const params = z.object({ organizationId: z.string().uuid() }).parse(request.params);
    await requireOrganizationMembership(request, params.organizationId);
    const body = deleteAssetSchema.parse(request.body ?? {});

    let assetPath = body.assetPath ? String(body.assetPath) : "";

    if (!assetPath && body.assetId && !String(body.assetId).startsWith("legacy:")) {
      const { data: assetRecord, error: assetError } = await supabaseAdmin
        .from("campaign_assets")
        .select("storage_path")
        .eq("organization_id", params.organizationId)
        .eq("id", body.assetId)
        .maybeSingle();

      if (assetError) {
        throw new AppError(400, "CAMPAIGN_ASSET_DELETE_FAILED", assetError.message);
      }

      assetPath = String(assetRecord?.storage_path ?? "");
    }

    if (!assetPath && body.assetId?.startsWith("legacy:")) {
      assetPath = body.assetId.slice("legacy:".length);
    }

    const expectedPrefix = `orgs/${params.organizationId}/campaign-assets/`;
    if (!assetPath || !assetPath.startsWith(expectedPrefix)) {
      throw new AppError(400, "CAMPAIGN_ASSET_DELETE_FAILED", "Asset path does not belong to this organization.");
    }

    const storageDelete = await supabaseAdmin.storage.from("brand-assets").remove([assetPath]);

    if (storageDelete.error) {
      throw new AppError(400, "CAMPAIGN_ASSET_DELETE_FAILED", storageDelete.error.message);
    }

    const { error: deleteError } = await supabaseAdmin
      .from("campaign_assets")
      .delete()
      .eq("organization_id", params.organizationId)
      .eq("storage_path", assetPath);

    if (deleteError) {
      throw new AppError(400, "CAMPAIGN_ASSET_DELETE_FAILED", deleteError.message);
    }

    return { success: true, assetPath };
  });

  app.get("/api/campaigns", async (request) => {
    const query = z.object({ organizationId: z.string().uuid() }).parse(request.query);
    await requireOrganizationMembership(request, query.organizationId);

    let { data, error } = await supabaseAdmin
      .from("campaigns")
      .select("*")
      .eq("organization_id", query.organizationId)
      .eq("is_reusable_template", false)
      .order("created_at", { ascending: false });

    if (isMissingColumnError(error, "is_reusable_template")) {
      ({ data, error } = await supabaseAdmin
        .from("campaigns")
        .select("*")
        .eq("organization_id", query.organizationId)
        .order("created_at", { ascending: false }));
    }

    if (error) {
      throw new AppError(400, "CAMPAIGN_LIST_FAILED", error.message);
    }

    return {
      items: data ?? []
    };
  });

  app.get("/api/campaigns/templates", async (request) => {
    const query = z.object({ organizationId: z.string().uuid() }).parse(request.query);
    await requireOrganizationMembership(request, query.organizationId);

    let [{ data: campaigns, error: campaignError }, { data: blocks, error: blockError }] = await Promise.all([
      supabaseAdmin
        .from("campaigns")
        .select("id, name, subject, editor_mode, updated_at")
        .eq("organization_id", query.organizationId)
        .eq("is_reusable_template", true)
        .order("updated_at", { ascending: false }),
      supabaseAdmin
        .from("campaign_blocks")
        .select("campaign_id, block_type, position")
    ]);

    if (isMissingColumnError(campaignError, "is_reusable_template")) {
      return {
        items: []
      };
    }

    if (campaignError) {
      throw new AppError(400, "CAMPAIGN_TEMPLATE_LIST_FAILED", campaignError.message);
    }

    if (blockError) {
      throw new AppError(400, "CAMPAIGN_TEMPLATE_LIST_FAILED", blockError.message);
    }

    const campaignIds = new Set((campaigns ?? []).map((campaign: any) => campaign.id));
    const blocksByCampaign = new Map<string, string[]>();

    ((blocks ?? []) as Array<Record<string, any>>)
      .filter((block) => campaignIds.has(String(block.campaign_id ?? "")))
      .sort((left, right) => Number(left.position ?? 0) - Number(right.position ?? 0))
      .forEach((block) => {
        const campaignId = String(block.campaign_id ?? "");
        if (!blocksByCampaign.has(campaignId)) blocksByCampaign.set(campaignId, []);
        blocksByCampaign.get(campaignId)?.push(String(block.block_type ?? ""));
      });

    return {
      items: (campaigns ?? []).map((campaign: any) => ({
        ...campaign,
        block_types: blocksByCampaign.get(String(campaign.id)) ?? []
      }))
    };
  });

  app.get("/api/campaigns/:id", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);

    const { data: blocks } = await supabaseAdmin
      .from("campaign_blocks")
      .select("id, block_type, position, content")
      .eq("campaign_id", params.id)
      .order("position", { ascending: true });

    return {
      campaign,
      sentSnapshot: campaign.sent_html_body
        ? {
            htmlBody: campaign.sent_html_body,
            textBody: campaign.sent_text_body ?? "",
            subject: campaign.sent_subject ?? campaign.subject ?? "",
            fromName: campaign.sent_from_name ?? campaign.from_name ?? "",
            fromEmail: campaign.sent_from_email ?? campaign.from_email ?? "",
            editorMode: campaign.sent_editor_mode ?? campaign.editor_mode ?? "html",
            capturedAt: campaign.sent_snapshot_at ?? campaign.sent_at ?? null,
          }
        : null,
      blocks: ((blocks ?? []) as Array<Record<string, any>>).map((block) => ({
        id: block.id,
        type: block.block_type,
        content: block.content
      }))
    };
  });

  app.get("/api/campaigns/:id/versions", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);

    const { data, error } = await supabaseAdmin
      .from("campaign_versions")
      .select("id, campaign_id, name, subject, from_name, from_email, editor_mode, save_source, created_at")
      .eq("campaign_id", params.id)
      .order("created_at", { ascending: false })
      .limit(5);

    if (error) {
      throw new AppError(400, "CAMPAIGN_VERSION_LIST_FAILED", error.message);
    }

    return {
      items: data ?? [],
    };
  });

  app.post("/api/campaigns", async (request) => {
    const payload = createCampaignSchema.parse(request.body);
    await requireOrganizationMembership(request, payload.organizationId);
    const user = await requireUser(request);
    await assertCampaignTitleAvailable({
      organizationId: payload.organizationId,
      actorAuthUserId: user.id,
      name: payload.name,
    });

    const { data, error } = await supabaseAdmin
      .from("campaigns")
      .insert({
        organization_id: payload.organizationId,
        name: payload.name,
        subject: payload.subject,
        from_name: payload.fromName,
        from_email: payload.fromEmail,
        editor_mode: payload.editorMode,
        status: "draft",
        created_by_auth_user_id: user.id,
        updated_by_auth_user_id: user.id,
        last_composed_at: isoNow(),
        last_activity_at: isoNow(),
        created_at: isoNow(),
        updated_at: isoNow()
      })
      .select("*")
      .single();

    if (error || !data) {
      throw new AppError(400, "CAMPAIGN_CREATE_FAILED", error?.message ?? "Unable to create campaign.");
    }

    await logCampaignActivity({
      organizationId: payload.organizationId,
      campaignId: String(data.id),
      actorAuthUserId: user.id,
      action: "campaign.created",
      metadata: {
        editorMode: payload.editorMode,
        status: "draft",
      }
    });

    return {
      campaign: data
    };
  });

  app.put("/api/campaigns/:id", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const payload = updateCampaignSchema.parse(request.body);
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);
    await assertCampaignTitleAvailable({
      organizationId: String(campaign.organization_id),
      actorAuthUserId: user.id,
      name: payload.name,
      excludeCampaignId: params.id,
    });

    const { data, error } = await supabaseAdmin
      .from("campaigns")
      .update({
        name: payload.name,
        subject: payload.subject,
        from_name: payload.fromName,
        from_email: payload.fromEmail,
        updated_by_auth_user_id: user.id,
        last_composed_at: isoNow(),
        last_activity_at: isoNow(),
        updated_at: isoNow()
      })
      .eq("id", params.id)
      .select("*")
      .single();

    if (error || !data) {
      throw new AppError(400, "CAMPAIGN_UPDATE_FAILED", error?.message ?? "Unable to update campaign.");
    }

    await logCampaignActivity({
      organizationId: String(campaign.organization_id),
      campaignId: params.id,
      actorAuthUserId: user.id,
      action: "campaign.details_updated",
      metadata: {
        name: payload.name,
        subject: payload.subject,
        fromName: payload.fromName,
        fromEmail: payload.fromEmail,
      }
    });

    return {
      campaign: data
    };
  });

  app.put("/api/campaigns/:id/content", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const payload = updateContentSchema.parse(request.body);
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);

    await persistCampaignContent({
      campaignId: params.id,
      editorMode: payload.editorMode,
      htmlBody: payload.htmlBody,
      textBody: payload.textBody,
      blocks: payload.blocks as CampaignBlock[] | undefined,
      actorAuthUserId: user.id
    });

    await logCampaignActivity({
      organizationId: String(campaign.organization_id),
      campaignId: params.id,
      actorAuthUserId: user.id,
      action: "campaign.content_updated",
      metadata: {
        editorMode: payload.editorMode,
        blockCount: Array.isArray(payload.blocks) ? payload.blocks.length : 0,
        hasHtmlBody: typeof payload.htmlBody === "string" && payload.htmlBody.length > 0,
        hasTextBody: typeof payload.textBody === "string" && payload.textBody.length > 0,
      }
    });

    return {
      success: true
    };
  });

  app.post("/api/campaigns/:id/versions", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const payload = createVersionSchema.parse(request.body ?? {});
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);

    const version = await createCampaignVersionSnapshot(params.id, user.id, payload.source);

    await logCampaignActivity({
      organizationId: String(campaign.organization_id),
      campaignId: params.id,
      actorAuthUserId: user.id,
      action: "campaign.version_saved",
      metadata: {
        versionId: version.id,
        source: payload.source,
      }
    });

    return {
      version,
    };
  });

  app.post("/api/campaigns/:id/versions/:versionId/restore", async (request) => {
    const params = z.object({ id: z.string().uuid(), versionId: z.string().uuid() }).parse(request.params);
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);

    const { data: version, error } = await supabaseAdmin
      .from("campaign_versions")
      .select("*")
      .eq("id", params.versionId)
      .eq("campaign_id", params.id)
      .maybeSingle();

    if (error) {
      throw new AppError(400, "CAMPAIGN_VERSION_LOOKUP_FAILED", error.message);
    }

    if (!version) {
      throw new AppError(404, "CAMPAIGN_VERSION_NOT_FOUND", "Campaign version not found.");
    }

    const { error: updateError } = await supabaseAdmin
      .from("campaigns")
      .update({
        name: version.name,
        subject: version.subject,
        from_name: version.from_name,
        from_email: version.from_email,
        updated_by_auth_user_id: user.id,
        last_composed_at: isoNow(),
        last_activity_at: isoNow(),
        updated_at: isoNow(),
      })
      .eq("id", params.id);

    if (updateError) {
      throw new AppError(400, "CAMPAIGN_VERSION_RESTORE_FAILED", updateError.message);
    }

    await persistCampaignContent({
      campaignId: params.id,
      editorMode: version.editor_mode === "html" ? "html" : "builder",
      htmlBody: version.html_body ?? "",
      textBody: version.text_body ?? "",
      blocks: Array.isArray(version.blocks) ? version.blocks as CampaignBlock[] : [],
      actorAuthUserId: user.id,
    });

    const restoredVersion = await createCampaignVersionSnapshot(params.id, user.id, "restore");

    await logCampaignActivity({
      organizationId: String(campaign.organization_id),
      campaignId: params.id,
      actorAuthUserId: user.id,
      action: "campaign.version_restored",
      metadata: {
        restoredFromVersionId: params.versionId,
        restoredToVersionId: restoredVersion.id,
      }
    });

    return {
      success: true,
      version: restoredVersion,
    };
  });

  app.post("/api/campaigns/:id/publish", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);

    const version = await createCampaignVersionSnapshot(params.id, user.id, "publish");
    const publishedAt = String(version.created_at ?? isoNow());

    const { data, error } = await supabaseAdmin
      .from("campaigns")
      .update({
        last_published_at: publishedAt,
        last_published_by_auth_user_id: user.id,
        last_published_version_id: version.id,
        updated_by_auth_user_id: user.id,
        last_activity_at: publishedAt,
        updated_at: publishedAt,
      })
      .eq("id", params.id)
      .select("*")
      .single();

    if (error || !data) {
      throw new AppError(400, "CAMPAIGN_PUBLISH_FAILED", error?.message ?? "Unable to publish campaign.");
    }

    await logCampaignActivity({
      organizationId: String(campaign.organization_id),
      campaignId: params.id,
      actorAuthUserId: user.id,
      action: "campaign.published",
      metadata: {
        publishedAt,
        versionId: version.id,
      }
    });

    return {
      success: true,
      campaign: data,
      version,
    };
  });

  app.post("/api/campaigns/:id/test-send", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const body = z.object({
      toEmail: z.string().email().optional(),
      toEmails: z.array(z.string().email()).max(5).optional()
    }).parse(request.body ?? {});
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);

    const toEmails = Array.from(new Set([...(body.toEmails ?? []), ...(body.toEmail ? [body.toEmail] : [])]));
    if (!toEmails.length) {
      throw new AppError(400, "TEST_EMAIL_REQUIRED", "Provide at least one valid test email address.");
    }

    const result = await sendCampaignTest(params.id, toEmails, provider);

    const { error: traceError } = await supabaseAdmin
      .from("campaigns")
      .update({
        last_test_sent_at: isoNow(),
        last_test_sent_by_auth_user_id: user.id,
        updated_by_auth_user_id: user.id,
        last_activity_at: isoNow(),
        updated_at: isoNow()
      })
      .eq("id", params.id);

    if (traceError) {
      throw new AppError(400, "CAMPAIGN_TEST_TRACE_FAILED", traceError.message);
    }

    await logCampaignActivity({
      organizationId: String(campaign.organization_id),
      campaignId: params.id,
      actorAuthUserId: user.id,
      action: "campaign.test_sent",
      metadata: {
        recipientCount: toEmails.length,
        recipients: toEmails,
      }
    });

    return {
      success: true,
      result
    };
  });

  app.post("/api/campaigns/:id/send", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const body = sendCampaignSchema.parse(request.body ?? {});
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);

    await queueCampaignSend(params.id, provider, body.contactIds, user.id);

    return {
      success: true,
      status: "queued"
    };
  });

  app.post("/api/campaigns/:id/schedule", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const body = scheduleCampaignSchema.parse(request.body ?? {});
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);

    await scheduleCampaignSend(params.id, body.scheduledFor, body.contactIds, user.id);

    return {
      success: true,
      status: "scheduled"
    };
  });

  app.post("/api/campaigns/:id/cancel", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);

    const status = await cancelCampaignSend(params.id, user.id);

    return {
      success: true,
      status
    };
  });

  app.post("/api/campaigns/:id/clone", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const body = cloneCampaignSchema.parse(request.body ?? {});
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);
    const nextName = body.name?.trim()
      || (body.asReusableTemplate ? `${campaign.name} reusable` : `${campaign.name} copy`);
    await assertCampaignTitleAvailable({
      organizationId: String(campaign.organization_id),
      actorAuthUserId: user.id,
      name: nextName,
    });

    const clonedCampaign = await cloneCampaign(params.id, {
      asReusableTemplate: body.asReusableTemplate,
      name: body.name,
      actorAuthUserId: user.id
    });

    return {
      campaign: clonedCampaign
    };
  });

  app.delete("/api/campaigns/:id", async (request) => {
    const params = z.object({ id: z.string().uuid() }).parse(request.params);
    const campaign = await loadCampaign(params.id);
    await requireOrganizationMembership(request, campaign.organization_id);
    const user = await requireUser(request);

    if (campaign.status === "sending") {
      throw new AppError(400, "CAMPAIGN_DELETE_BLOCKED", "Cannot delete a campaign that is currently sending.");
    }

    await logCampaignActivity({
      organizationId: String(campaign.organization_id),
      campaignId: params.id,
      actorAuthUserId: user.id,
      action: "campaign.deleted",
      metadata: {
        name: campaign.name,
        status: campaign.status,
      }
    });

    await supabaseAdmin.from("campaign_blocks").delete().eq("campaign_id", params.id);
    await supabaseAdmin.from("campaigns").delete().eq("id", params.id);

    return { success: true };
  });
}
