import crypto from 'crypto';
import { Prisma, UserRole, CampaignModerationStatus, PetitionSignatureStatus } from '@prisma/client';
import { prisma } from '../../../config/database';
import { AppError } from '../../../middleware/error-handler';
import { hasAnyRole, ADMIN_ROLES } from '../../../utils/roles';
import { geoipService } from '../../../services/geoip.service';
import { emailService } from '../../../services/email.service';
import { logger } from '../../../utils/logger';
import type {
  CreatePetitionInput, UpdatePetitionInput, ListPetitionsInput,
  SignPetitionInput, ListSignaturesInput, UpdateSignatureStatusInput,
  ModeratePetitionInput, ListModerationQueueInput,
} from './petitions.schemas';

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

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.petition.findUnique({
      where: { slug: candidate },
      select: { id: true },
    });

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

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

function hashToken(token: string): string {
  return crypto.createHash('sha256').update(token).digest('hex');
}

const petitionSelect = {
  id: true,
  slug: true,
  title: true,
  description: true,
  signatureGoal: true,
  showProgress: true,
  showSignatureCount: true,
  showSignerNames: true,
  signatureCountOffset: true,
  requireName: true,
  requireEmail: true,
  requirePostalCode: true,
  requirePhone: true,
  allowComment: true,
  commentLabel: true,
  requireEmailConfirmation: true,
  coverPhoto: true,
  coverVideoId: true,
  callToAction: true,
  thankYouMessage: true,
  highlightPetition: true,
  linkedCampaignId: true,
  status: true,
  isUserGenerated: true,
  moderationStatus: true,
  rejectionReason: true,
  moderationNotes: true,
  createdByUserId: true,
  createdByUserEmail: true,
  createdByUserName: true,
  reviewedByUserId: true,
  reviewedAt: true,
  createdAt: true,
  updatedAt: true,
  _count: {
    select: { signatures: true },
  },
} satisfies Prisma.PetitionSelect;

const publicPetitionSelect = {
  id: true,
  slug: true,
  title: true,
  description: true,
  signatureGoal: true,
  showProgress: true,
  showSignatureCount: true,
  showSignerNames: true,
  signatureCountOffset: true,
  requireName: true,
  requireEmail: true,
  requirePostalCode: true,
  requirePhone: true,
  allowComment: true,
  commentLabel: true,
  requireEmailConfirmation: true,
  coverPhoto: true,
  coverVideoId: true,
  callToAction: true,
  thankYouMessage: true,
  highlightPetition: true,
  linkedCampaignId: true,
  linkedCampaign: {
    select: {
      id: true,
      slug: true,
      title: true,
      description: true,
      status: true,
    },
  },
  status: true,
  createdByUserName: true,
  createdAt: true,
  _count: {
    select: {
      signatures: {
        where: { status: { in: ['VERIFIED', 'UNVERIFIED'] } },
      },
    },
  },
} satisfies Prisma.PetitionSelect;

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

const MILESTONE_THRESHOLDS = [10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000];

export const petitionsService = {
  // ─── Admin CRUD ──────────────────────────────────────────────────────

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

    const where: Prisma.PetitionWhereInput = {};

    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 petitions
    if (user && !hasAnyRole(user, ADMIN_ROLES)) {
      where.createdByUserId = user.id;
    }

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

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

  async findById(id: string) {
    const petition = await prisma.petition.findUnique({
      where: { id },
      select: petitionSelect,
    });

    if (!petition) throw new AppError(404, 'Petition not found', 'PETITION_NOT_FOUND');
    return petition;
  },

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

    return prisma.petition.create({
      data: {
        ...data,
        slug,
        createdByUserId: user.id,
        createdByUserEmail: user.email,
        createdByUserName: (user as any).name || user.email,
      },
      select: petitionSelect,
    });
  },

  async update(id: string, data: UpdatePetitionInput) {
    const existing = await prisma.petition.findUnique({
      where: { id },
      select: { id: true, slug: true, title: true },
    });

    if (!existing) throw new AppError(404, 'Petition not found', 'PETITION_NOT_FOUND');

    // Regenerate slug if title changed
    let slug: string | undefined;
    if (data.title && data.title !== existing.title) {
      slug = await resolveSlugCollision(generateSlug(data.title), id);
    }

    return prisma.petition.update({
      where: { id },
      data: {
        ...data,
        ...(slug && { slug }),
      },
      select: petitionSelect,
    });
  },

  async delete(id: string) {
    const existing = await prisma.petition.findUnique({
      where: { id },
      select: { id: true },
    });

    if (!existing) throw new AppError(404, 'Petition not found', 'PETITION_NOT_FOUND');

    await prisma.petition.delete({ where: { id } });
  },

  // ─── Public Routes ───────────────────────────────────────────────────

  async findActivePetitions(page: number = 1, limit: number = 20) {
    const where = {
      status: 'ACTIVE' as const,
      OR: [
        { isUserGenerated: false },
        { isUserGenerated: true, moderationStatus: 'APPROVED' as const },
      ],
    };
    const skip = (page - 1) * limit;

    const [petitions, total] = await Promise.all([
      prisma.petition.findMany({
        where,
        select: publicPetitionSelect,
        orderBy: [
          { highlightPetition: 'desc' },
          { createdAt: 'desc' },
        ],
        skip,
        take: limit,
      }),
      prisma.petition.count({ where }),
    ]);

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

  async findBySlugPublic(slug: string) {
    const petition = await prisma.petition.findFirst({
      where: {
        slug,
        status: 'ACTIVE',
        OR: [
          { isUserGenerated: false },
          { isUserGenerated: true, moderationStatus: 'APPROVED' },
        ],
      },
      select: publicPetitionSelect,
    });

    if (!petition) throw new AppError(404, 'Petition not found', 'PETITION_NOT_FOUND');
    return petition;
  },

  async signPetition(slug: string, data: SignPetitionInput, ip: string) {
    const petition = await prisma.petition.findFirst({
      where: { slug, status: 'ACTIVE' },
      select: {
        id: true,
        slug: true,
        title: true,
        requireName: true,
        requireEmail: true,
        requirePostalCode: true,
        requirePhone: true,
        requireEmailConfirmation: true,
        confirmationEmailSubject: true,
        confirmationEmailBody: true,
        signatureCountOffset: true,
      },
    });

    if (!petition) throw new AppError(404, 'Petition not found or not active', 'PETITION_NOT_FOUND');

    // Validate required fields based on petition config
    if (petition.requireName && !data.signerName) {
      throw new AppError(400, 'Name is required', 'VALIDATION_ERROR');
    }
    if (petition.requireEmail && !data.signerEmail) {
      throw new AppError(400, 'Email is required', 'VALIDATION_ERROR');
    }
    if (petition.requirePostalCode && !data.signerPostalCode) {
      throw new AppError(400, 'Postal code is required', 'VALIDATION_ERROR');
    }
    if (petition.requirePhone && !data.signerPhone) {
      throw new AppError(400, 'Phone number is required', 'VALIDATION_ERROR');
    }

    // Deduplicate by email (same response for duplicates to prevent email enumeration)
    if (data.signerEmail) {
      const existing = await prisma.petitionSignature.findFirst({
        where: { petitionId: petition.id, signerEmail: data.signerEmail.toLowerCase() },
        select: { id: true },
      });
      if (existing) {
        return { id: existing.id, status: 'UNVERIFIED' as const, verificationSent: false, alreadySigned: true };
      }
    }

    // GeoIP lookup
    const geo = await geoipService.lookup(ip);

    // Compute display name
    let displayName: string | null = null;
    if (data.isAnonymous) {
      displayName = 'Anonymous';
    } else if (data.signerName) {
      displayName = escapeHtml(data.signerName);
    }

    // Verification token handling
    let verificationToken: string | null = null;
    let verificationTokenHash: string | null = null;
    const needsVerification = petition.requireEmailConfirmation && data.signerEmail;
    if (needsVerification) {
      verificationToken = crypto.randomBytes(32).toString('hex');
      verificationTokenHash = hashToken(verificationToken);
    }

    const signatureStatus: PetitionSignatureStatus = needsVerification ? 'PENDING_VERIFICATION' : 'UNVERIFIED';

    // Create signature
    const signature = await prisma.petitionSignature.create({
      data: {
        petitionId: petition.id,
        signerName: data.signerName ? escapeHtml(data.signerName) : null,
        signerEmail: data.signerEmail?.toLowerCase() || null,
        signerPostalCode: data.signerPostalCode || null,
        signerPhone: data.signerPhone || null,
        signerComment: data.signerComment ? escapeHtml(data.signerComment) : null,
        isAnonymous: data.isAnonymous ?? false,
        displayName,
        status: signatureStatus,
        verificationToken: verificationTokenHash,
        verificationSentAt: needsVerification ? new Date() : null,
        signerIp: ip,
        geoCountry: geo?.country || null,
        geoRegion: geo?.region || null,
        geoCity: geo?.city || null,
      },
    });

    // CRM contact upsert
    if (data.signerEmail) {
      try {
        let contact = await prisma.contact.findFirst({
          where: { email: data.signerEmail.toLowerCase() },
          select: { id: true },
        });

        if (!contact) {
          contact = await prisma.contact.create({
            data: {
              displayName: data.signerName ? escapeHtml(data.signerName) : data.signerEmail,
              email: data.signerEmail.toLowerCase(),
              primarySource: 'PETITION_SIGNER',
            },
          });
        }

        // Link signature to contact
        await prisma.petitionSignature.update({
          where: { id: signature.id },
          data: { contactId: contact.id },
        });

        // Record activity
        await prisma.contactActivity.create({
          data: {
            contactId: contact.id,
            type: 'PETITION_SIGNED',
            title: `Signed petition: ${petition.title}`,
            metadata: { petitionId: petition.id, petitionSlug: petition.slug } as unknown as Prisma.InputJsonValue,
          },
        });
      } catch (err) {
        logger.warn('Failed to upsert CRM contact for petition signature', { error: (err as Error).message });
      }
    }

    // Send confirmation email if required
    let verificationSent = false;
    if (needsVerification && verificationToken && data.signerEmail) {
      try {
        const verificationUrl = `${process.env.APP_URL || 'http://localhost:4000'}/api/petitions/verify/${verificationToken}`;
        await emailService.sendEmail({
          to: data.signerEmail,
          subject: petition.confirmationEmailSubject || `Confirm your signature — ${petition.title}`,
          html: petition.confirmationEmailBody
            ? petition.confirmationEmailBody
                .replace(/\{\{NAME\}\}/g, data.signerName || 'Supporter')
                .replace(/\{\{PETITION_TITLE\}\}/g, escapeHtml(petition.title))
                .replace(/\{\{VERIFICATION_URL\}\}/g, verificationUrl)
            : `<p>Hi ${escapeHtml(data.signerName || 'there')},</p>
               <p>Please confirm your signature on "<strong>${escapeHtml(petition.title)}</strong>" by clicking the link below:</p>
               <p><a href="${verificationUrl}">Confirm my signature</a></p>
               <p>If you did not sign this petition, you can safely ignore this email.</p>`,
          text: `Confirm your signature on "${petition.title}": ${verificationUrl}`,
        });
        verificationSent = true;
      } catch (err) {
        logger.warn('Failed to send petition verification email', { error: (err as Error).message });
      }
    }

    // Check milestone thresholds
    try {
      const totalCount = await prisma.petitionSignature.count({
        where: {
          petitionId: petition.id,
          status: { in: ['VERIFIED', 'UNVERIFIED'] },
        },
      });

      const displayCount = totalCount + petition.signatureCountOffset;

      for (const threshold of MILESTONE_THRESHOLDS) {
        if (displayCount >= threshold && (displayCount - 1) < threshold) {
          logger.info(`Petition "${petition.title}" reached ${threshold} signatures`);
          break;
        }
      }
    } catch (err) {
      logger.warn('Failed to check petition milestones', { error: (err as Error).message });
    }

    return {
      id: signature.id,
      status: signatureStatus,
      verificationSent,
      alreadySigned: false,
    };
  },

  async verifySignature(token: string) {
    const tokenHash = hashToken(token);

    const signature = await prisma.petitionSignature.findFirst({
      where: { verificationToken: tokenHash },
      select: { id: true, status: true, petitionId: true, verificationSentAt: true },
    });

    if (!signature) throw new AppError(404, 'Invalid or expired verification link', 'INVALID_TOKEN');

    // Check 30-day expiry
    if (signature.verificationSentAt) {
      const expiryMs = 30 * 24 * 60 * 60 * 1000;
      if (Date.now() - signature.verificationSentAt.getTime() > expiryMs) {
        throw new AppError(410, 'Verification link has expired', 'TOKEN_EXPIRED');
      }
    }

    if (signature.status === 'VERIFIED') {
      return { alreadyVerified: true };
    }

    await prisma.petitionSignature.update({
      where: { id: signature.id },
      data: { status: 'VERIFIED', verifiedAt: new Date() },
    });

    return { alreadyVerified: false };
  },

  // ─── Signatures Admin ────────────────────────────────────────────────

  async listSignaturesAdmin(petitionId: string, filters: ListSignaturesInput) {
    const { page, limit, search, status } = filters;
    const skip = (page - 1) * limit;

    const where: Prisma.PetitionSignatureWhereInput = { petitionId };

    if (search) {
      where.OR = [
        { signerName: { contains: search, mode: 'insensitive' } },
        { signerEmail: { contains: search, mode: 'insensitive' } },
        { signerPostalCode: { contains: search, mode: 'insensitive' } },
      ];
    }

    if (status) where.status = status as PetitionSignatureStatus;

    const [signatures, total] = await Promise.all([
      prisma.petitionSignature.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: limit,
      }),
      prisma.petitionSignature.count({ where }),
    ]);

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

  /**
   * Public signature feed — returns only opaque record IDs + timestamps + anonymity flag.
   * PII fields (displayName, signerComment, geoCity, geoCountry) are NEVER exposed
   * on the public endpoint, regardless of the petition's `showSignerNames` setting.
   * Admins can still view full signer data via the authenticated admin endpoint.
   * Hardened 2026-04-12 after red-team audit found this vector was being scraped
   * to build activist dossiers.
   */
  async listSignaturesPublic(slug: string, page: number = 1, limit: number = 20) {
    const petition = await prisma.petition.findFirst({
      where: { slug, status: 'ACTIVE' },
      select: { id: true },
    });

    if (!petition) throw new AppError(404, 'Petition not found', 'PETITION_NOT_FOUND');

    const where: Prisma.PetitionSignatureWhereInput = {
      petitionId: petition.id,
      status: { in: ['VERIFIED', 'UNVERIFIED'] },
    };

    const [signatures, total] = await Promise.all([
      prisma.petitionSignature.findMany({
        where,
        select: {
          id: true,
          isAnonymous: true,
          createdAt: true,
        },
        orderBy: { createdAt: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      prisma.petitionSignature.count({ where }),
    ]);

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

  async updateSignatureStatus(id: string, data: UpdateSignatureStatusInput) {
    const existing = await prisma.petitionSignature.findUnique({
      where: { id },
      select: { id: true },
    });
    if (!existing) throw new AppError(404, 'Signature not found', 'SIGNATURE_NOT_FOUND');

    return prisma.petitionSignature.update({
      where: { id },
      data: { status: data.status as PetitionSignatureStatus },
    });
  },

  async deleteSignature(id: string) {
    const existing = await prisma.petitionSignature.findUnique({
      where: { id },
      select: { id: true },
    });
    if (!existing) throw new AppError(404, 'Signature not found', 'SIGNATURE_NOT_FOUND');

    await prisma.petitionSignature.delete({ where: { id } });
  },

  async bulkUpdateSignatureStatus(ids: string[], status: PetitionSignatureStatus) {
    return prisma.petitionSignature.updateMany({
      where: { id: { in: ids } },
      data: { status },
    });
  },

  // ─── Stats ───────────────────────────────────────────────────────────

  async getStats(petitionId: string) {
    const petition = await prisma.petition.findUnique({
      where: { id: petitionId },
      select: { id: true, signatureGoal: true, signatureCountOffset: true },
    });

    if (!petition) throw new AppError(404, 'Petition not found', 'PETITION_NOT_FOUND');

    const countWhere = {
      petitionId,
      status: { in: ['VERIFIED' as const, 'UNVERIFIED' as const] },
    };

    const [total, byCountry, byRegion] = await Promise.all([
      prisma.petitionSignature.count({ where: countWhere }),
      prisma.petitionSignature.groupBy({
        by: ['geoCountry'],
        where: { ...countWhere, geoCountry: { not: null } },
        _count: true,
        orderBy: { _count: { geoCountry: 'desc' } },
        take: 20,
      }),
      prisma.petitionSignature.groupBy({
        by: ['geoRegion'],
        where: { ...countWhere, geoRegion: { not: null } },
        _count: true,
        orderBy: { _count: { geoRegion: 'desc' } },
        take: 20,
      }),
    ]);

    const displayTotal = total + petition.signatureCountOffset;

    // NOTE: `recentSigners` (names + cities) was removed 2026-04-12 to prevent
    // unauthenticated scraping of activist identities. Admin UIs should fetch
    // signer details via the authenticated admin signatures endpoint.
    return {
      total: displayTotal,
      verified: total,
      goal: petition.signatureGoal,
      percentComplete: petition.signatureGoal
        ? Math.min(100, Math.round((displayTotal / petition.signatureGoal) * 100))
        : null,
      byCountry: Object.fromEntries(byCountry.map(c => [c.geoCountry, c._count])),
      byRegion: Object.fromEntries(byRegion.map(r => [r.geoRegion, r._count])),
    };
  },

  // ─── Export ──────────────────────────────────────────────────────────

  async exportSignatures(petitionId: string, format: 'csv' | 'json') {
    const petition = await prisma.petition.findUnique({
      where: { id: petitionId },
      select: { slug: true },
    });

    if (!petition) throw new AppError(404, 'Petition not found', 'PETITION_NOT_FOUND');

    const signatures = await prisma.petitionSignature.findMany({
      where: { petitionId },
      orderBy: { createdAt: 'desc' },
    });

    if (format === 'json') {
      return { data: signatures, filename: `petition-${petition.slug}-signatures.json` };
    }

    // CSV format
    const headers = ['Name', 'Email', 'Postal Code', 'Phone', 'Comment', 'Anonymous', 'Country', 'Region', 'City', 'Status', 'Date'];
    const csvRows = [headers.join(',')];

    for (const sig of signatures) {
      const row = [
        sig.signerName || '',
        sig.signerEmail || '',
        sig.signerPostalCode || '',
        sig.signerPhone || '',
        (sig.signerComment || '').replace(/"/g, '""'),
        sig.isAnonymous ? 'Yes' : 'No',
        sig.geoCountry || '',
        sig.geoRegion || '',
        sig.geoCity || '',
        sig.status,
        sig.createdAt.toISOString(),
      ].map(v => `"${v}"`);
      csvRows.push(row.join(','));
    }

    return { data: csvRows.join('\n'), filename: `petition-${petition.slug}-signatures.csv` };
  },

  // ─── Moderation ──────────────────────────────────────────────────────

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

    const where: Prisma.PetitionWhereInput = { isUserGenerated: true };

    if (moderationStatus) {
      where.moderationStatus = moderationStatus;
    } else {
      where.moderationStatus = 'PENDING_REVIEW';
    }

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

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

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

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

    return { pending, approved, rejected, changesRequested };
  },

  async moderatePetition(id: string, data: ModeratePetitionInput, reviewerId: string) {
    const petition = await prisma.petition.findUnique({
      where: { id },
      select: { id: true, isUserGenerated: true },
    });

    if (!petition) throw new AppError(404, 'Petition not found', 'PETITION_NOT_FOUND');
    if (!petition.isUserGenerated) throw new AppError(400, 'Only user-generated petitions can be moderated', 'NOT_USER_GENERATED');

    const statusMap: Record<string, CampaignModerationStatus> = {
      approve: 'APPROVED',
      reject: 'REJECTED',
      request_changes: 'CHANGES_REQUESTED',
    };

    return prisma.petition.update({
      where: { id },
      data: {
        moderationStatus: statusMap[data.action],
        status: data.action === 'approve' ? 'ACTIVE' : undefined,
        rejectionReason: data.reason || null,
        moderationNotes: data.notes || null,
        reviewedByUserId: reviewerId,
        reviewedAt: new Date(),
      },
      select: petitionSelect,
    });
  },
};
