import {
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { AuditAction, AuditEntityType } from '@aechr/shared';
import {
  AssessmentType,
  AssignmentStatus,
  CampaignAutoAssignMode,
  CampaignStatus,
  Prisma,
  RecurrenceFrequency,
  UserStatus,
} from '@prisma/client';
import type { JwtAuthUser } from '../../common/interfaces/jwt-auth-user.interface';
import { buildPagination, buildSoftDeleteWhere } from '../../common/prisma/pagination';
import { AuditService } from '../audit/audit.service';
import { PrismaService } from '../prisma/prisma.service';
import { CampaignQueryDto } from './dto/campaign-query.dto';
import { CreateCampaignDto } from './dto/create-campaign.dto';
import { GenerateMonthlyCampaignDto } from './dto/generate-monthly-campaign.dto';
import { UpdateCampaignDto } from './dto/update-campaign.dto';

const campaignListSelect = Prisma.validator<Prisma.AssessmentCampaignSelect>()({
  id: true,
  title: true,
  assessmentType: true,
  templateId: true,
  periodMonth: true,
  periodYear: true,
  startDate: true,
  endDate: true,
  status: true,
  isAutoGenerated: true,
  recurrenceFrequency: true,
  recurrenceInterval: true,
  autoAssignMode: true,
  autoAssignEmployeeIds: true,
  nextRunAt: true,
  lastGeneratedAt: true,
  sourceCampaignId: true,
  createdAt: true,
  updatedAt: true,
  deletedAt: true,
  createdBy: true,
  updatedBy: true,
  deletedBy: true,
  template: {
    select: {
      id: true,
      name: true,
      code: true,
      assessmentType: true,
      description: true,
      version: true,
      isActive: true,
      createdAt: true,
      updatedAt: true,
      deletedAt: true,
      createdBy: true,
      updatedBy: true,
      deletedBy: true,
    },
  },
});

@Injectable()
export class CampaignsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  async list(query: CampaignQueryDto) {
    const { skip, take, meta } = buildPagination(query);
    const where: Prisma.AssessmentCampaignWhereInput = {
      ...buildSoftDeleteWhere(query.includeDeleted),
      ...(query.assessmentType ? { assessmentType: query.assessmentType } : {}),
      ...(query.status ? { status: query.status } : {}),
      ...(query.periodMonth ? { periodMonth: query.periodMonth } : {}),
      ...(query.periodYear ? { periodYear: query.periodYear } : {}),
      ...(query.search ? { title: { contains: query.search } } : {}),
    };

    const [items, total] = await Promise.all([
      this.prisma.assessmentCampaign.findMany({
        where,
        skip,
        take,
        select: campaignListSelect,
        orderBy: { createdAt: query.sortOrder },
      }),
      this.prisma.assessmentCampaign.count({ where }),
    ]);
    return { items, total, ...meta };
  }

  async create(dto: CreateCampaignDto, actor: JwtAuthUser) {
    await this.ensureTemplate(dto.templateId, dto.assessmentType);
    await this.ensureNoDuplicateMonthly(dto.assessmentType, dto.periodMonth, dto.periodYear);

    const campaign = await this.prisma.assessmentCampaign.create({
      data: {
        title: dto.title,
        assessmentType: dto.assessmentType,
        templateId: dto.templateId,
        periodMonth: dto.periodMonth,
        periodYear: dto.periodYear,
        startDate: new Date(dto.startDate),
        endDate: new Date(dto.endDate),
        status: dto.status,
        isAutoGenerated: dto.isAutoGenerated,
        recurrenceFrequency: dto.recurrenceFrequency,
        recurrenceInterval: dto.recurrenceInterval,
        autoAssignMode: dto.autoAssignMode,
        autoAssignEmployeeIds: this.normalizeAutoAssignEmployeeIds(dto.autoAssignEmployeeIds),
        nextRunAt: this.resolveNextRunAt(dto.startDate, dto.recurrenceFrequency),
        createdBy: actor.sub,
        updatedBy: actor.sub,
      },
      select: campaignListSelect,
    });

    await this.auditService.log({
      module: 'campaigns',
      action: AuditAction.CREATE,
      entityType: AuditEntityType.CAMPAIGN,
      entityId: campaign.id,
      newValues: dto as unknown as Prisma.InputJsonValue,
      createdBy: actor.sub,
      updatedBy: actor.sub,
    });
    return campaign;
  }

  async findOne(id: string) {
    const campaign = await this.prisma.assessmentCampaign.findUnique({
      where: { id },
      include: {
        template: {
          include: {
            questions: {
              where: { deletedAt: null },
              include: { question: true },
              orderBy: { sortOrder: 'asc' },
            },
          },
        },
        assignments: {
          where: { deletedAt: null },
          include: { employee: true },
        },
      },
    });
    if (!campaign) throw new NotFoundException('Campaign not found');
    return campaign;
  }

  async update(id: string, dto: UpdateCampaignDto, actor: JwtAuthUser) {
    const existing = await this.findOne(id);
    await this.ensureTemplate(dto.templateId ?? existing.templateId, dto.assessmentType ?? existing.assessmentType);
    if (
      dto.assessmentType !== undefined ||
      dto.periodMonth !== undefined ||
      dto.periodYear !== undefined
    ) {
      await this.ensureNoDuplicateMonthly(
        dto.assessmentType ?? existing.assessmentType,
        dto.periodMonth ?? existing.periodMonth ?? undefined,
        dto.periodYear ?? existing.periodYear ?? undefined,
        id,
      );
    }

    const updated = await this.prisma.assessmentCampaign.update({
      where: { id },
      data: {
        title: dto.title,
        assessmentType: dto.assessmentType,
        templateId: dto.templateId,
        periodMonth: dto.periodMonth,
        periodYear: dto.periodYear,
        startDate: dto.startDate ? new Date(dto.startDate) : undefined,
        endDate: dto.endDate ? new Date(dto.endDate) : undefined,
        status: dto.status,
        isAutoGenerated: dto.isAutoGenerated,
        recurrenceFrequency: dto.recurrenceFrequency,
        recurrenceInterval: dto.recurrenceInterval,
        autoAssignMode: dto.autoAssignMode,
        autoAssignEmployeeIds:
          dto.autoAssignEmployeeIds !== undefined
            ? this.normalizeAutoAssignEmployeeIds(dto.autoAssignEmployeeIds)
            : undefined,
        nextRunAt:
          dto.recurrenceFrequency !== undefined || dto.startDate !== undefined
            ? this.resolveNextRunAt(
                dto.startDate ?? existing.startDate.toISOString(),
                dto.recurrenceFrequency ?? existing.recurrenceFrequency,
              )
            : undefined,
        updatedBy: actor.sub,
      },
    });

    await this.auditService.log({
      module: 'campaigns',
      action: AuditAction.UPDATE,
      entityType: AuditEntityType.CAMPAIGN,
      entityId: id,
      newValues: dto as unknown as Prisma.InputJsonValue,
      createdBy: actor.sub,
      updatedBy: actor.sub,
    });
    return updated;
  }

  async remove(id: string, actor: JwtAuthUser) {
    await this.findOne(id);
    const deleted = await this.prisma.assessmentCampaign.update({
      where: { id },
      data: { deletedAt: new Date(), deletedBy: actor.sub, updatedBy: actor.sub },
    });
    await this.auditService.log({
      module: 'campaigns',
      action: AuditAction.DELETE,
      entityType: AuditEntityType.CAMPAIGN,
      entityId: id,
      remarks: 'Campaign soft deleted',
      createdBy: actor.sub,
      updatedBy: actor.sub,
      deletedBy: actor.sub,
    });
    return deleted;
  }

  async restore(id: string, actor: JwtAuthUser) {
    await this.findOne(id);
    const restored = await this.prisma.assessmentCampaign.update({
      where: { id },
      data: { deletedAt: null, deletedBy: null, updatedBy: actor.sub },
    });
    await this.auditService.log({
      module: 'campaigns',
      action: AuditAction.RESTORE,
      entityType: AuditEntityType.CAMPAIGN,
      entityId: id,
      remarks: 'Campaign restored',
      createdBy: actor.sub,
      updatedBy: actor.sub,
    });
    return restored;
  }

  async generateMonthly(dto: GenerateMonthlyCampaignDto, actor: JwtAuthUser) {
    const now = new Date();
    const periodMonth = dto.periodMonth ?? (now.getUTCMonth() === 0 ? 12 : now.getUTCMonth());
    const periodYear = dto.periodYear ?? (now.getUTCMonth() === 0 ? now.getUTCFullYear() - 1 : now.getUTCFullYear());

    const existing = await this.prisma.assessmentCampaign.findFirst({
      where: {
        assessmentType: AssessmentType.MONTHLY,
        periodMonth,
        periodYear,
        deletedAt: null,
      },
    });
    if (existing) {
      return existing;
    }

    const template = await this.prisma.assessmentTemplate.findFirst({
      where: {
        assessmentType: AssessmentType.MONTHLY,
        isActive: true,
        deletedAt: null,
      },
      orderBy: [{ version: 'desc' }, { createdAt: 'desc' }],
    });
    if (!template) {
      throw new NotFoundException('No active monthly template found');
    }

    const startDate = new Date(Date.UTC(periodYear, periodMonth - 1, 1));
    const endDate = new Date(Date.UTC(periodYear, periodMonth - 1, 10, 23, 59, 59));

    return this.create(
      {
        title: `Monthly Assessment ${periodMonth}/${periodYear}`,
        assessmentType: AssessmentType.MONTHLY,
        templateId: template.id,
        periodMonth,
        periodYear,
        startDate: startDate.toISOString(),
        endDate: endDate.toISOString(),
        status: CampaignStatus.ACTIVE,
        isAutoGenerated: true,
        recurrenceFrequency: RecurrenceFrequency.NONE,
        recurrenceInterval: 1,
        autoAssignMode: CampaignAutoAssignMode.NONE,
      },
      actor,
    );
  }

  async runRecurringGeneration(actor: JwtAuthUser) {
    const now = new Date();
    const recurringCampaigns = await this.prisma.assessmentCampaign.findMany({
      where: {
        deletedAt: null,
        sourceCampaignId: null,
        recurrenceFrequency: { not: RecurrenceFrequency.NONE },
        nextRunAt: { lte: now },
      },
      orderBy: { nextRunAt: 'asc' },
    });

    let generatedCount = 0;
    let assignmentCount = 0;

    for (const campaign of recurringCampaigns) {
      let nextRunAt = campaign.nextRunAt;
      let lastGeneratedAt = campaign.lastGeneratedAt;
      let guard = 0;

      while (nextRunAt && nextRunAt <= now && guard < 24) {
        const { generated, assignmentsCreated } = await this.generateRecurringOccurrence(
          campaign,
          nextRunAt,
          actor,
        );
        if (generated) {
          generatedCount += 1;
          assignmentCount += assignmentsCreated;
          lastGeneratedAt = nextRunAt;
        }
        nextRunAt = this.advanceDate(nextRunAt, campaign.recurrenceFrequency, campaign.recurrenceInterval);
        guard += 1;
      }

      await this.prisma.assessmentCampaign.update({
        where: { id: campaign.id },
        data: {
          nextRunAt,
          lastGeneratedAt,
          updatedBy: actor.sub,
        },
      });
    }

    return {
      generatedCount,
      assignmentCount,
      recurringCampaignsProcessed: recurringCampaigns.length,
    };
  }

  private async ensureTemplate(templateId: string, assessmentType: AssessmentType) {
    const template = await this.prisma.assessmentTemplate.findFirst({
      where: { id: templateId, deletedAt: null },
    });
    if (!template) throw new NotFoundException('Template not found');
    if (template.assessmentType !== assessmentType) {
      throw new ConflictException('Template assessment type mismatch');
    }
  }

  private async ensureNoDuplicateMonthly(
    assessmentType: AssessmentType,
    periodMonth?: number,
    periodYear?: number,
    excludeId?: string,
  ) {
    if (assessmentType !== AssessmentType.MONTHLY || !periodMonth || !periodYear) return;
    const existing = await this.prisma.assessmentCampaign.findFirst({
      where: {
        assessmentType,
        periodMonth,
        periodYear,
        deletedAt: null,
        ...(excludeId ? { id: { not: excludeId } } : {}),
      },
    });
    if (existing) throw new ConflictException('Monthly campaign already exists for this month/year');
  }

  private normalizeAutoAssignEmployeeIds(employeeIds?: string[]) {
    if (!employeeIds?.length) {
      return Prisma.JsonNull;
    }

    return employeeIds as Prisma.InputJsonValue;
  }

  private normalizeStoredAutoAssignEmployeeIds(value: Prisma.JsonValue | null) {
    return value === null ? Prisma.JsonNull : (value as Prisma.InputJsonValue);
  }

  private resolveNextRunAt(startDate: string, recurrenceFrequency: RecurrenceFrequency) {
    if (recurrenceFrequency === RecurrenceFrequency.NONE) {
      return null;
    }

    return new Date(startDate);
  }

  private advanceDate(date: Date, frequency: RecurrenceFrequency, interval: number) {
    const next = new Date(date);

    if (frequency === RecurrenceFrequency.WEEKLY) {
      next.setUTCDate(next.getUTCDate() + interval * 7);
      return next;
    }

    if (frequency === RecurrenceFrequency.MONTHLY) {
      next.setUTCMonth(next.getUTCMonth() + interval);
      return next;
    }

    if (frequency === RecurrenceFrequency.YEARLY) {
      next.setUTCFullYear(next.getUTCFullYear() + interval);
      return next;
    }

    return next;
  }

  private getGeneratedPeriod(startDate: Date) {
    return {
      periodMonth: startDate.getUTCMonth() + 1,
      periodYear: startDate.getUTCFullYear(),
    };
  }

  private buildGeneratedTitle(
    base: { title: string; recurrenceFrequency: RecurrenceFrequency },
    startDate: Date,
  ) {
    if (base.recurrenceFrequency === RecurrenceFrequency.MONTHLY) {
      const { periodMonth, periodYear } = this.getGeneratedPeriod(startDate);
      return `${base.title} ${String(periodMonth).padStart(2, '0')}/${periodYear}`;
    }

    if (base.recurrenceFrequency === RecurrenceFrequency.YEARLY) {
      return `${base.title} ${startDate.getUTCFullYear()}`;
    }

    if (base.recurrenceFrequency === RecurrenceFrequency.WEEKLY) {
      return `${base.title} ${startDate.toISOString().slice(0, 10)}`;
    }

    return base.title;
  }

  private async generateRecurringOccurrence(
    campaign: {
      id: string;
      title: string;
      assessmentType: AssessmentType;
      templateId: string;
      startDate: Date;
      endDate: Date;
      status: CampaignStatus;
      recurrenceFrequency: RecurrenceFrequency;
      autoAssignMode: CampaignAutoAssignMode;
      autoAssignEmployeeIds: Prisma.JsonValue | null;
    },
    occurrenceStart: Date,
    actor: JwtAuthUser,
  ) {
    const existing = await this.prisma.assessmentCampaign.findFirst({
      where: {
        sourceCampaignId: campaign.id,
        startDate: occurrenceStart,
        deletedAt: null,
      },
    });

    if (existing) {
      return { generated: false, assignmentsCreated: 0 };
    }

    const durationMs = campaign.endDate.getTime() - campaign.startDate.getTime();
    const occurrenceEnd = new Date(occurrenceStart.getTime() + durationMs);
    const { periodMonth, periodYear } = this.getGeneratedPeriod(occurrenceStart);
    const shouldStoreMonthlyPeriod =
      campaign.recurrenceFrequency === RecurrenceFrequency.MONTHLY ||
      campaign.assessmentType === AssessmentType.MONTHLY;

    const created = await this.prisma.assessmentCampaign.create({
      data: {
        title: this.buildGeneratedTitle(campaign, occurrenceStart),
        assessmentType: campaign.assessmentType,
        templateId: campaign.templateId,
        periodMonth: shouldStoreMonthlyPeriod ? periodMonth : null,
        periodYear: shouldStoreMonthlyPeriod || campaign.recurrenceFrequency === RecurrenceFrequency.YEARLY ? periodYear : null,
        startDate: occurrenceStart,
        endDate: occurrenceEnd,
        status: campaign.status === CampaignStatus.DRAFT ? CampaignStatus.DRAFT : CampaignStatus.ACTIVE,
        isAutoGenerated: true,
        recurrenceFrequency: RecurrenceFrequency.NONE,
        recurrenceInterval: 1,
        autoAssignMode: campaign.autoAssignMode,
        autoAssignEmployeeIds: this.normalizeStoredAutoAssignEmployeeIds(campaign.autoAssignEmployeeIds),
        sourceCampaignId: campaign.id,
        createdBy: actor.sub,
        updatedBy: actor.sub,
      },
    });

    const assignmentsCreated = await this.autoAssignCampaign(created.id, campaign, actor);

    await this.auditService.log({
      module: 'campaigns',
      action: AuditAction.CREATE,
      entityType: AuditEntityType.CAMPAIGN,
      entityId: created.id,
      remarks: `Recurring campaign generated from ${campaign.id}`,
      newValues: {
        sourceCampaignId: campaign.id,
        startDate: occurrenceStart.toISOString(),
      } as Prisma.InputJsonValue,
      createdBy: actor.sub,
      updatedBy: actor.sub,
    });

    return { generated: true, assignmentsCreated };
  }

  private async autoAssignCampaign(
    campaignId: string,
    sourceCampaign: {
      autoAssignMode: CampaignAutoAssignMode;
      autoAssignEmployeeIds: Prisma.JsonValue | null;
    },
    actor: JwtAuthUser,
  ) {
    if (sourceCampaign.autoAssignMode === CampaignAutoAssignMode.NONE) {
      return 0;
    }

    const employeeIds =
      sourceCampaign.autoAssignMode === CampaignAutoAssignMode.ALL_ACTIVE_EMPLOYEES
        ? (
            await this.prisma.user.findMany({
              where: {
                deletedAt: null,
                status: UserStatus.ACTIVE,
              },
              select: { id: true },
            })
          ).map((item) => item.id)
        : Array.isArray(sourceCampaign.autoAssignEmployeeIds)
          ? sourceCampaign.autoAssignEmployeeIds.map(String)
          : [];

    if (!employeeIds.length) {
      return 0;
    }

    const employees = await this.prisma.user.findMany({
      where: {
        id: { in: employeeIds },
        deletedAt: null,
        status: UserStatus.ACTIVE,
      },
      select: { id: true },
    });

    let createdCount = 0;
    for (const employee of employees) {
      const existing = await this.prisma.assessmentAssignment.findFirst({
        where: { campaignId, employeeId: employee.id },
      });

      if (existing && !existing.deletedAt) {
        continue;
      }

      if (existing?.deletedAt) {
        await this.prisma.assessmentAssignment.update({
          where: { id: existing.id },
          data: {
            deletedAt: null,
            deletedBy: null,
            isEnabled: true,
            enabledBy: actor.sub,
            enabledAt: new Date(),
            status: AssignmentStatus.PENDING,
            assignedBy: actor.sub,
            updatedBy: actor.sub,
          },
        });
        createdCount += 1;
        continue;
      }

      await this.prisma.assessmentAssignment.create({
        data: {
          campaignId,
          employeeId: employee.id,
          assignedBy: actor.sub,
          isEnabled: true,
          enabledBy: actor.sub,
          enabledAt: new Date(),
          status: AssignmentStatus.PENDING,
          createdBy: actor.sub,
          updatedBy: actor.sub,
        },
      });
      createdCount += 1;
    }

    return createdCount;
  }
}
