import { AppError } from "@/core/errors/app-error";
import { EmailEventsRepository } from "@/modules/ses-webhooks/repositories/email-events-repository";
import { CampaignsRepository, CampaignWithLogsAndSender } from "../repositories/campaigns-repository";

interface GetCampaignByIdUseCaseRequest {
  id: string;
  userId: string;
}

export interface CampaignOpenerReport {
  contactId: string;
  email: string;
  name: string | null;
  firstOpenedAt: Date;
  openCount: number;
}

export interface OpenTimelinePoint {
  label: string;
  opens: number;
}

interface GetCampaignByIdUseCaseResponse {
  campaign: CampaignWithLogsAndSender;
  summary: {
    total: number;
    success: number;
    failed: number;
    pending: number;
  };
  opens: {
    total: number;
    unique: number;
    rate: number;
    openers: CampaignOpenerReport[];
    timeline: OpenTimelinePoint[];
  };
}

const HOURLY_WINDOW_MS = 48 * 60 * 60 * 1000;

export class GetCampaignByIdUseCase {
  constructor(
    private campaignsRepository: CampaignsRepository,
    private emailEventsRepository: EmailEventsRepository
  ) {}

  async execute({ id, userId }: GetCampaignByIdUseCaseRequest): Promise<GetCampaignByIdUseCaseResponse> {
    const campaign = await this.campaignsRepository.findByIdWithLogsAndSender(id);

    if (campaign?.createdById !== userId) {
      throw AppError.notFound("Campanha não encontrada");
    }

    const summary = {
      total: campaign.logs.length,
      success: campaign.logs.filter((log) => log.status === "SUCCESS").length,
      failed: campaign.logs.filter((log) => log.status === "FAILED").length,
      pending: campaign.logs.filter((log) => log.status === "PENDING").length,
    };

    const [openStats, openerGroups, openEvents] = await Promise.all([
      this.emailEventsRepository.getCampaignOpenStats(id),
      this.emailEventsRepository.findOpenersByCampaignId(id),
      this.emailEventsRepository.findOpenEventsByCampaignId(id),
    ]);

    const contactsById = new Map(campaign.logs.map((log) => [log.contact.id, log.contact]));

    const openers: CampaignOpenerReport[] = openerGroups
      .map((group) => {
        const contact = contactsById.get(group.contactId);
        return {
          contactId: group.contactId,
          email: contact?.email ?? "—",
          name: contact?.name ?? null,
          firstOpenedAt: group.firstOpenedAt,
          openCount: group.openCount,
        };
      })
      .sort((a, b) => a.firstOpenedAt.getTime() - b.firstOpenedAt.getTime());

    const firstSentAt = campaign.logs
      .map((log) => log.sentAt)
      .filter((date): date is Date => date !== null)
      .sort((a, b) => a.getTime() - b.getTime())[0];

    const rate =
      summary.success > 0 ? Number(((openStats.uniqueOpens / summary.success) * 100).toFixed(1)) : 0;

    return {
      campaign,
      summary,
      opens: {
        total: openStats.totalOpens,
        unique: openStats.uniqueOpens,
        rate,
        openers,
        timeline: this.buildTimeline(openEvents, firstSentAt),
      },
    };
  }

  private buildTimeline(events: { createdAt: Date }[], anchor: Date | undefined): OpenTimelinePoint[] {
    if (events.length === 0) return [];

    const start = anchor ?? events[0].createdAt;
    const boundary = new Date(start.getTime() + HOURLY_WINDOW_MS);

    const buckets = new Map<string, { label: string; order: number; opens: number }>();

    for (const event of events) {
      const eventDate = new Date(event.createdAt);
      let key: string;
      let label: string;
      let order: number;

      if (eventDate < boundary) {
        const hourStart = new Date(eventDate);
        hourStart.setMinutes(0, 0, 0);
        key = hourStart.toISOString();
        label = `${hourStart.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit" })}h`;
        order = hourStart.getTime();
      } else {
        const dayStart = new Date(eventDate);
        dayStart.setHours(0, 0, 0, 0);
        key = dayStart.toISOString();
        label = dayStart.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
        order = dayStart.getTime();
      }

      const existing = buckets.get(key);
      if (existing) {
        existing.opens += 1;
      } else {
        buckets.set(key, { label, order, opens: 1 });
      }
    }

    return [...buckets.values()]
      .sort((a, b) => a.order - b.order)
      .map(({ label, opens }) => ({ label, opens }));
  }
}
