import { Prisma, UserRole, CampaignModerationStatus } from '@prisma/client';
import { prisma } from '../../../config/database';
import { AppError } from '../../../middleware/error-handler';
import { hasAnyRole, ADMIN_ROLES } from '../../../utils/roles';
import type {
  CreateCampaignInput, UpdateCampaignInput, ListCampaignsInput,
  CreateUserCampaignInput, UpdateUserCampaignInput, ModerateCampaignInput, ListModerationQueueInput,
} from './campaigns.schemas';

function escapeHtml(unsafe: string): string {
  return unsafe
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

/**
 * SUPER_ADMIN-only select: includes creator email and internal moderation fields
 * (notes, reviewer ID, rejection reason). These are deliberately hidden from
 * other admin roles to prevent cross-admin PII/moderation-intel leakage.
 * Split from single `campaignSelect` on 2026-04-12.
 */
const superAdminCampaignSelect = {
  id: true,
  slug: true,
  title: true,
  description: true,
  emailSubject: true,
  emailBody: true,
  callToAction: true,
  coverPhoto: true,
  coverVideoId: true,
  status: true,
  allowSmtpEmail: true,
  allowMailtoLink: true,
  collectUserInfo: true,
  showEmailCount: true,
  showCallCount: true,
  allowEmailEditing: true,
  allowCustomRecipients: true,
  showResponseWall: true,
  highlightCampaign: true,
  targetGovernmentLevels: true,
  createdByUserId: true,
  createdByUserEmail: true,
  createdByUserName: true,
  isUserGenerated: true,
  moderationStatus: true,
  reviewedByUserId: true,
  reviewedAt: true,
  rejectionReason: true,
  moderationNotes: true,
  createdAt: true,
  updatedAt: true,
  _count: {
    select: {
      emails: true,
      responses: true,
    },
  },
} satisfies Prisma.CampaignSelect;

/** Non-super-admin select: excludes creator email + internal moderation fields. */
const adminCampaignSelect = {
  id: true,
  slug: true,
  title: true,
  description: true,
  emailSubject: true,
  emailBody: true,
  callToAction: true,
  coverPhoto: true,
  coverVideoId: true,
  status: true,
  allowSmtpEmail: true,
  allowMailtoLink: true,
  collectUserInfo: true,
  showEmailCount: true,
  showCallCount: true,
  allowEmailEditing: true,
  allowCustomRecipients: true,
  showResponseWall: true,
  highlightCampaign: true,
  targetGovernmentLevels: true,
  createdByUserId: true,
  createdByUserName: true,
  isUserGenerated: true,
  moderationStatus: true,
  reviewedAt: true,
  createdAt: true,
  updatedAt: true,
  _count: {
    select: {
      emails: true,
      responses: true,
    },
  },
} satisfies Prisma.CampaignSelect;

function pickCampaignSelect(user?: { role: UserRole } | null) {
  return user?.role === UserRole.SUPER_ADMIN ? superAdminCampaignSelect : adminCampaignSelect;
}

/**
 * Ownership enforcement for admin endpoints that mutate or expose single-campaign
 * data. SUPER_ADMIN bypasses; any other admin must own the campaign. Throws 404
 * (not 403) to avoid leaking which campaign IDs exist. Added 2026-04-12.
 */
async function assertCampaignAccess(id: string, user: AuthUser): Promise<void> {
  const c = await prisma.campaign.findUnique({
    where: { id },
    select: { createdByUserId: true },
  });
  if (!c) throw new AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
  if (user.role === UserRole.SUPER_ADMIN) return;
  if (c.createdByUserId !== user.id) {
    throw new AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
  }
}

/** Public-facing select — strips admin-only fields (emails, internal IDs, moderation notes) */
const publicCampaignSelect = {
  id: true,
  slug: true,
  title: true,
  description: true,
  emailSubject: true,
  emailBody: true,
  callToAction: true,
  coverPhoto: true,
  coverVideoId: true,
  status: true,
  allowSmtpEmail: true,
  allowMailtoLink: true,
  collectUserInfo: true,
  showEmailCount: true,
  showCallCount: true,
  allowEmailEditing: true,
  allowCustomRecipients: true,
  showResponseWall: true,
  highlightCampaign: true,
  targetGovernmentLevels: true,
  createdByUserName: true,
  isUserGenerated: true,
  moderationStatus: true,
  createdAt: true,
  updatedAt: true,
  _count: {
    select: {
      emails: true,
      responses: true,
    },
  },
} satisfies Prisma.CampaignSelect;

function generateSlug(title: string): string {
  return title
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 80);
}

async function resolveSlugCollision(slug: string, excludeId?: string): Promise<string> {
  let candidate = slug;
  let suffix = 2;

  while (true) {
    const existing = await prisma.campaign.findUnique({
      where: { slug: candidate },
      select: { id: true },
    });

    if (!existing || (excludeId && existing.id === excludeId)) {
      return candidate;
    }

    candidate = `${slug}-${suffix}`;
    suffix++;
  }
}

interface AuthUser {
  id: string;
  email: string;
  role: UserRole;
}

export const campaignsService = {
  async findAll(filters: ListCampaignsInput, user?: AuthUser) {
    const { page, limit, search, status } = filters;
    const skip = (page - 1) * limit;

    const where: Prisma.CampaignWhereInput = {};

    if (search) {
      where.OR = [
        { title: { contains: search, mode: 'insensitive' } },
        { description: { contains: search, mode: 'insensitive' } },
      ];
    }

    if (status) where.status = status;

    // Non-admin users only see their own campaigns
    if (user && !hasAnyRole(user, ADMIN_ROLES)) {
      where.createdByUserId = user.id;
    }

    const [campaigns, total] = await Promise.all([
      prisma.campaign.findMany({
        where,
        select: pickCampaignSelect(user),
        skip,
        take: limit,
        orderBy: { createdAt: 'desc' },
      }),
      prisma.campaign.count({ where }),
    ]);

    return {
      campaigns,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  },

  /** Fetch a campaign by ID with ownership enforcement. SUPER_ADMIN bypasses. */
  async findById(id: string, user: AuthUser) {
    await assertCampaignAccess(id, user);
    const campaign = await prisma.campaign.findUnique({
      where: { id },
      select: pickCampaignSelect(user),
    });

    if (!campaign) {
      throw new AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
    }

    return campaign;
  },

  /** Fetch by slug (admin path). Still enforces ownership. */
  async findBySlug(slug: string, user: AuthUser) {
    const campaign = await prisma.campaign.findUnique({
      where: { slug },
      select: pickCampaignSelect(user),
    });

    if (!campaign) {
      throw new AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
    }

    if (user.role !== UserRole.SUPER_ADMIN && campaign.createdByUserId !== user.id) {
      throw new AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
    }

    return campaign;
  },

  async create(data: CreateCampaignInput, user: AuthUser) {
    const baseSlug = generateSlug(data.title);
    const slug = await resolveSlugCollision(baseSlug);

    // Look up user name from DB
    const dbUser = await prisma.user.findUnique({
      where: { id: user.id },
      select: { name: true },
    });

    // If highlighting this campaign, unset any other highlighted campaign
    if (data.highlightCampaign) {
      await prisma.campaign.updateMany({
        where: { highlightCampaign: true },
        data: { highlightCampaign: false },
      });
    }

    const campaign = await prisma.campaign.create({
      data: {
        ...data,
        slug,
        createdByUserId: user.id,
        createdByUserEmail: user.email,
        createdByUserName: dbUser?.name ?? null,
      },
      select: pickCampaignSelect(user),
    });

    return campaign;
  },

  async update(id: string, data: UpdateCampaignInput, user: AuthUser) {
    // Ownership check (SUPER_ADMIN bypasses). Prevents cross-admin campaign edits.
    await assertCampaignAccess(id, user);

    const existing = await prisma.campaign.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
    }

    const updateData: Prisma.CampaignUpdateInput = { ...data };

    // Regenerate slug if title changes
    if (data.title && data.title !== existing.title) {
      const baseSlug = generateSlug(data.title);
      updateData.slug = await resolveSlugCollision(baseSlug, id);
    }

    // If highlighting this campaign, unset any other highlighted campaign
    if (data.highlightCampaign) {
      await prisma.campaign.updateMany({
        where: { highlightCampaign: true, id: { not: id } },
        data: { highlightCampaign: false },
      });
    }

    const campaign = await prisma.campaign.update({
      where: { id },
      data: updateData,
      select: pickCampaignSelect(user),
    });

    return campaign;
  },

  async findActiveCampaigns(page: number = 1, limit: number = 20) {
    const where = { status: 'ACTIVE' as const };
    const skip = (page - 1) * limit;

    const [campaigns, total] = await Promise.all([
      prisma.campaign.findMany({
        where,
        select: publicCampaignSelect,
        orderBy: [
          { highlightCampaign: 'desc' },
          { createdAt: 'desc' },
        ],
        skip,
        take: limit,
      }),
      prisma.campaign.count({ where }),
    ]);

    return {
      campaigns,
      pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
    };
  },

  async findBySlugPublic(slug: string) {
    const campaign = await prisma.campaign.findUnique({
      where: { slug },
      select: publicCampaignSelect,
    });

    if (!campaign) {
      throw new AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
    }

    if (campaign.status !== 'ACTIVE') {
      throw new AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
    }

    return campaign;
  },

  async delete(id: string, user: AuthUser) {
    // Ownership check (SUPER_ADMIN bypasses).
    await assertCampaignAccess(id, user);
    await prisma.campaign.delete({ where: { id } });
  },

  // --- User-Generated Campaign Methods ---

  async createUserCampaign(data: CreateUserCampaignInput, user: AuthUser) {
    const baseSlug = generateSlug(data.title);
    const slug = await resolveSlugCollision(baseSlug);

    const dbUser = await prisma.user.findUnique({
      where: { id: user.id },
      select: { name: true },
    });

    const campaign = await prisma.campaign.create({
      data: {
        slug,
        title: escapeHtml(data.title),
        description: data.description ? escapeHtml(data.description) : null,
        emailSubject: escapeHtml(data.emailSubject),
        emailBody: escapeHtml(data.emailBody),
        callToAction: data.callToAction ? escapeHtml(data.callToAction) : null,
        targetGovernmentLevels: data.targetGovernmentLevels,
        status: 'DRAFT',
        isUserGenerated: true,
        moderationStatus: CampaignModerationStatus.PENDING_REVIEW,
        allowSmtpEmail: false,
        allowMailtoLink: true,
        collectUserInfo: true,
        showEmailCount: true,
        showCallCount: false,
        allowEmailEditing: false,
        allowCustomRecipients: false,
        showResponseWall: false,
        highlightCampaign: false,
        createdByUserId: user.id,
        createdByUserEmail: user.email,
        createdByUserName: dbUser?.name ?? null,
      },
      select: pickCampaignSelect(user),
    });

    return campaign;
  },

  async findUserCampaigns(userId: string) {
    // Self-view endpoint — safe to use reduced select (user already knows own email).
    return prisma.campaign.findMany({
      where: { createdByUserId: userId, isUserGenerated: true },
      select: adminCampaignSelect,
      orderBy: { createdAt: 'desc' },
    });
  },

  async updateUserCampaign(id: string, data: Partial<CreateUserCampaignInput>, user: AuthUser) {
    const existing = await prisma.campaign.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
    }
    if (existing.createdByUserId !== user.id) {
      throw new AppError(403, 'You can only edit your own campaigns', 'FORBIDDEN');
    }
    if (!existing.isUserGenerated) {
      throw new AppError(403, 'Cannot edit admin-created campaigns', 'FORBIDDEN');
    }
    if (
      existing.moderationStatus !== CampaignModerationStatus.CHANGES_REQUESTED &&
      existing.moderationStatus !== CampaignModerationStatus.PENDING_REVIEW
    ) {
      throw new AppError(400, 'Campaign cannot be edited in its current state', 'INVALID_STATE');
    }

    const updateData: Prisma.CampaignUncheckedUpdateInput = {};
    if (data.title) {
      updateData.title = escapeHtml(data.title);
      const baseSlug = generateSlug(data.title);
      updateData.slug = await resolveSlugCollision(baseSlug, id);
    }
    if (data.description !== undefined) updateData.description = data.description ? escapeHtml(data.description) : null;
    if (data.emailSubject) updateData.emailSubject = escapeHtml(data.emailSubject);
    if (data.emailBody) updateData.emailBody = escapeHtml(data.emailBody);
    if (data.callToAction !== undefined) updateData.callToAction = data.callToAction ? escapeHtml(data.callToAction) : null;
    if (data.targetGovernmentLevels) updateData.targetGovernmentLevels = data.targetGovernmentLevels;

    // Reset to pending review on edit
    updateData.moderationStatus = CampaignModerationStatus.PENDING_REVIEW;
    updateData.rejectionReason = null;

    return prisma.campaign.update({
      where: { id },
      data: updateData,
      select: pickCampaignSelect(user),
    });
  },

  // --- Moderation Methods ---

  async findModerationQueue(filters: ListModerationQueueInput, user: AuthUser) {
    const { page, limit, search, moderationStatus } = filters;
    const skip = (page - 1) * limit;

    const where: Prisma.CampaignWhereInput = { isUserGenerated: true };
    if (moderationStatus) where.moderationStatus = moderationStatus;
    if (search) {
      where.OR = [
        { title: { contains: search, mode: 'insensitive' } },
        { createdByUserName: { contains: search, mode: 'insensitive' } },
        { createdByUserEmail: { contains: search, mode: 'insensitive' } },
      ];
    }

    const [campaigns, total] = await Promise.all([
      prisma.campaign.findMany({
        where,
        select: pickCampaignSelect(user),
        skip,
        take: limit,
        orderBy: { createdAt: 'desc' },
      }),
      prisma.campaign.count({ where }),
    ]);

    return {
      campaigns,
      pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
    };
  },

  async getModerationStats() {
    const [total, pending, approved, rejected, changesRequested] = await Promise.all([
      prisma.campaign.count({ where: { isUserGenerated: true } }),
      prisma.campaign.count({ where: { moderationStatus: CampaignModerationStatus.PENDING_REVIEW } }),
      prisma.campaign.count({ where: { moderationStatus: CampaignModerationStatus.APPROVED } }),
      prisma.campaign.count({ where: { moderationStatus: CampaignModerationStatus.REJECTED } }),
      prisma.campaign.count({ where: { moderationStatus: CampaignModerationStatus.CHANGES_REQUESTED } }),
    ]);
    return { total, pending, approved, rejected, changesRequested };
  },

  async moderateCampaign(id: string, input: ModerateCampaignInput, reviewer: AuthUser) {
    const existing = await prisma.campaign.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
    }
    if (!existing.isUserGenerated) {
      throw new AppError(400, 'Only user-generated campaigns can be moderated', 'INVALID_STATE');
    }

    const updateData: Prisma.CampaignUncheckedUpdateInput = {
      reviewedByUserId: reviewer.id,
      reviewedAt: new Date(),
      moderationNotes: input.notes ?? null,
    };

    switch (input.action) {
      case 'approve':
        updateData.moderationStatus = CampaignModerationStatus.APPROVED;
        updateData.status = 'ACTIVE';
        updateData.rejectionReason = null;
        break;
      case 'reject':
        updateData.moderationStatus = CampaignModerationStatus.REJECTED;
        updateData.rejectionReason = input.reason ?? null;
        break;
      case 'request_changes':
        updateData.moderationStatus = CampaignModerationStatus.CHANGES_REQUESTED;
        updateData.rejectionReason = input.reason ?? null;
        break;
    }

    return prisma.campaign.update({
      where: { id },
      data: updateData,
      select: pickCampaignSelect(reviewer),
    });
  },
};
