/**
 * Report Schedule Service
 * Manages scheduled reports, recipients, and schedule executions
 *
 * @module Reports
 */

import { Repository, FindOptionsWhere, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
import {
  ReportSchedule,
  DeliveryMethod,
  ScheduleExecutionStatus,
  ScheduleExportFormat,
} from '../entities/report-schedule.entity';
import { ReportRecipient } from '../entities/report-recipient.entity';
import { ScheduleExecution } from '../entities/schedule-execution.entity';

export interface ScheduleSearchParams {
  tenantId: string;
  reportDefinitionId?: string;
  isActive?: boolean;
  deliveryMethod?: DeliveryMethod;
  limit?: number;
  offset?: number;
}

export interface CreateScheduleDto {
  reportDefinitionId: string;
  name: string;
  cronExpression: string;
  timezone?: string;
  parameters?: Record<string, any>;
  deliveryMethod?: DeliveryMethod;
  deliveryConfig?: Record<string, any>;
  exportFormat?: ScheduleExportFormat;
  isActive?: boolean;
}

export interface UpdateScheduleDto extends Partial<Omit<CreateScheduleDto, 'reportDefinitionId'>> {}

export interface CreateRecipientDto {
  scheduleId: string;
  userId?: string | null;
  email?: string | null;
  name?: string | null;
  isActive?: boolean;
}

export class ReportScheduleService {
  constructor(
    private readonly scheduleRepository: Repository<ReportSchedule>,
    private readonly recipientRepository: Repository<ReportRecipient>,
    private readonly scheduleExecutionRepository: Repository<ScheduleExecution>
  ) {}

  // ==================== Schedule CRUD ====================

  async findAll(params: ScheduleSearchParams): Promise<{ data: ReportSchedule[]; total: number }> {
    const {
      tenantId,
      reportDefinitionId,
      isActive,
      deliveryMethod,
      limit = 50,
      offset = 0,
    } = params;

    const where: FindOptionsWhere<ReportSchedule> = { tenantId };

    if (reportDefinitionId) {
      where.reportDefinitionId = reportDefinitionId;
    }

    if (isActive !== undefined) {
      where.isActive = isActive;
    }

    if (deliveryMethod) {
      where.deliveryMethod = deliveryMethod;
    }

    const [data, total] = await this.scheduleRepository.findAndCount({
      where,
      relations: ['reportDefinition', 'recipients'],
      take: limit,
      skip: offset,
      order: { name: 'ASC' },
    });

    return { data, total };
  }

  async findOne(id: string, tenantId: string): Promise<ReportSchedule | null> {
    return this.scheduleRepository.findOne({
      where: { id, tenantId },
      relations: ['reportDefinition', 'recipients'],
    });
  }

  async findByReportId(
    reportDefinitionId: string,
    tenantId: string
  ): Promise<ReportSchedule[]> {
    return this.scheduleRepository.find({
      where: { reportDefinitionId, tenantId },
      relations: ['recipients'],
      order: { name: 'ASC' },
    });
  }

  async create(
    tenantId: string,
    dto: CreateScheduleDto,
    createdBy?: string
  ): Promise<ReportSchedule> {
    const schedule = this.scheduleRepository.create({
      ...dto,
      tenantId,
      createdBy: createdBy || null,
      nextRunAt: this.calculateNextRunAt(dto.cronExpression, dto.timezone),
    });

    return this.scheduleRepository.save(schedule);
  }

  async update(
    id: string,
    tenantId: string,
    dto: UpdateScheduleDto
  ): Promise<ReportSchedule | null> {
    const schedule = await this.findOne(id, tenantId);
    if (!schedule) return null;

    Object.assign(schedule, dto);

    // Recalculate next run if cron expression changed
    if (dto.cronExpression) {
      schedule.nextRunAt = this.calculateNextRunAt(
        dto.cronExpression,
        dto.timezone || schedule.timezone
      );
    }

    return this.scheduleRepository.save(schedule);
  }

  async delete(id: string, tenantId: string): Promise<boolean> {
    const schedule = await this.findOne(id, tenantId);
    if (!schedule) return false;

    await this.scheduleRepository.delete(id);
    return true;
  }

  // ==================== Schedule Operations ====================

  async schedule(
    id: string,
    tenantId: string,
    cronExpression: string,
    options?: {
      timezone?: string;
      deliveryMethod?: DeliveryMethod;
      deliveryConfig?: Record<string, any>;
    }
  ): Promise<ReportSchedule | null> {
    const schedule = await this.findOne(id, tenantId);
    if (!schedule) return null;

    schedule.cronExpression = cronExpression;
    schedule.isActive = true;

    if (options?.timezone) {
      schedule.timezone = options.timezone;
    }

    if (options?.deliveryMethod) {
      schedule.deliveryMethod = options.deliveryMethod;
    }

    if (options?.deliveryConfig) {
      schedule.deliveryConfig = options.deliveryConfig;
    }

    schedule.nextRunAt = this.calculateNextRunAt(cronExpression, schedule.timezone);

    return this.scheduleRepository.save(schedule);
  }

  async toggleActive(id: string, tenantId: string): Promise<ReportSchedule | null> {
    const schedule = await this.findOne(id, tenantId);
    if (!schedule) return null;

    schedule.isActive = !schedule.isActive;

    if (schedule.isActive) {
      schedule.nextRunAt = this.calculateNextRunAt(schedule.cronExpression, schedule.timezone);
    } else {
      schedule.nextRunAt = null;
    }

    return this.scheduleRepository.save(schedule);
  }

  async getDueSchedules(): Promise<ReportSchedule[]> {
    const now = new Date();

    return this.scheduleRepository.find({
      where: {
        isActive: true,
        nextRunAt: LessThanOrEqual(now),
      },
      relations: ['reportDefinition', 'recipients'],
    });
  }

  async updateLastRun(
    id: string,
    status: ScheduleExecutionStatus,
    nextRunAt: Date | null
  ): Promise<void> {
    await this.scheduleRepository.update(id, {
      lastRunAt: new Date(),
      lastRunStatus: status,
      nextRunAt,
      runCount: () => 'run_count + 1',
    });
  }

  // ==================== Recipients CRUD ====================

  async getRecipients(scheduleId: string): Promise<ReportRecipient[]> {
    return this.recipientRepository.find({
      where: { scheduleId },
      order: { name: 'ASC' },
    });
  }

  async addRecipient(dto: CreateRecipientDto): Promise<ReportRecipient> {
    // Validate that either userId or email is provided
    if (!dto.userId && !dto.email) {
      throw new Error('Either userId or email must be provided');
    }

    const recipient = this.recipientRepository.create(dto);
    return this.recipientRepository.save(recipient);
  }

  async updateRecipient(
    id: string,
    dto: Partial<CreateRecipientDto>
  ): Promise<ReportRecipient | null> {
    const recipient = await this.recipientRepository.findOne({ where: { id } });
    if (!recipient) return null;

    Object.assign(recipient, dto);
    return this.recipientRepository.save(recipient);
  }

  async removeRecipient(id: string): Promise<boolean> {
    const result = await this.recipientRepository.delete(id);
    return (result.affected ?? 0) > 0;
  }

  async setRecipients(
    scheduleId: string,
    recipients: Array<{ userId?: string; email?: string; name?: string }>
  ): Promise<ReportRecipient[]> {
    // Remove existing recipients
    await this.recipientRepository.delete({ scheduleId });

    // Add new recipients
    const newRecipients = recipients.map((r) =>
      this.recipientRepository.create({
        scheduleId,
        userId: r.userId || null,
        email: r.email || null,
        name: r.name || null,
        isActive: true,
      })
    );

    return this.recipientRepository.save(newRecipients);
  }

  // ==================== Schedule Execution ====================

  async createScheduleExecution(
    scheduleId: string,
    executionId: string | null,
    status: ScheduleExecutionStatus
  ): Promise<ScheduleExecution> {
    const scheduleExecution = this.scheduleExecutionRepository.create({
      scheduleId,
      executionId,
      status,
      executedAt: new Date(),
    });

    return this.scheduleExecutionRepository.save(scheduleExecution);
  }

  async updateScheduleExecution(
    id: string,
    status: ScheduleExecutionStatus,
    recipientsNotified: number,
    deliveryStatus?: Record<string, any>,
    errorMessage?: string
  ): Promise<ScheduleExecution | null> {
    const scheduleExecution = await this.scheduleExecutionRepository.findOne({
      where: { id },
    });

    if (!scheduleExecution) return null;

    scheduleExecution.status = status;
    scheduleExecution.recipientsNotified = recipientsNotified;

    if (deliveryStatus) {
      scheduleExecution.deliveryStatus = deliveryStatus;
    }

    if (errorMessage) {
      scheduleExecution.errorMessage = errorMessage;
    }

    return this.scheduleExecutionRepository.save(scheduleExecution);
  }

  async getScheduleExecutionHistory(
    scheduleId: string,
    limit = 20
  ): Promise<ScheduleExecution[]> {
    return this.scheduleExecutionRepository.find({
      where: { scheduleId },
      relations: ['execution'],
      order: { executedAt: 'DESC' },
      take: limit,
    });
  }

  // ==================== History Operations ====================

  async getHistory(
    tenantId: string,
    options?: {
      scheduleId?: string;
      limit?: number;
      daysBack?: number;
    }
  ): Promise<ReportSchedule[]> {
    const where: FindOptionsWhere<ReportSchedule> = { tenantId };

    if (options?.daysBack) {
      const dateFrom = new Date();
      dateFrom.setDate(dateFrom.getDate() - options.daysBack);
      where.lastRunAt = MoreThanOrEqual(dateFrom);
    }

    return this.scheduleRepository.find({
      where,
      relations: ['reportDefinition', 'scheduleExecutions'],
      order: { lastRunAt: 'DESC' },
      take: options?.limit || 50,
    });
  }

  // ==================== Private Methods ====================

  private calculateNextRunAt(cronExpression: string, timezone?: string): Date | null {
    // This would use a cron parser library like cron-parser
    // For now, return a placeholder (next day at same time)
    const next = new Date();
    next.setDate(next.getDate() + 1);
    return next;
  }
}
