import { Repository, ILike, FindOptionsWhere, In } from 'typeorm';
import {
  ReportDefinition,
  ReportExecution,
  ReportSchedule,
  ReportRecipient,
  CustomReport,
  ExecutionStatus,
  ReportType,
} from '../entities';
import {
  CreateReportDefinitionDto,
  UpdateReportDefinitionDto,
  CreateReportScheduleDto,
  UpdateReportScheduleDto,
  CreateReportRecipientDto,
  UpdateReportRecipientDto,
  CreateCustomReportDto,
  UpdateCustomReportDto,
  ReportDefinitionFiltersDto,
  ReportScheduleFiltersDto,
  CustomReportFiltersDto,
} from '../dto';
import { NotFoundError, ValidationError, ConflictError } from '../../../shared/errors/index.js';
import { logger } from '../../../shared/utils/logger.js';

/**
 * ReportsService
 *
 * Manages report definitions, schedules, recipients, and custom reports.
 * Provides CRUD operations with multi-tenant isolation.
 */
export class ReportsService {
  constructor(
    private readonly definitionRepository: Repository<ReportDefinition>,
    private readonly scheduleRepository: Repository<ReportSchedule>,
    private readonly recipientRepository: Repository<ReportRecipient>,
    private readonly customReportRepository: Repository<CustomReport>
  ) {}

  // ==================== REPORT DEFINITIONS ====================

  /**
   * Get all report definitions with optional filtering
   */
  async findAllDefinitions(
    tenantId: string,
    filters: ReportDefinitionFiltersDto = {}
  ): Promise<{ data: ReportDefinition[]; total: number }> {
    const { reportType, category, isPublic, isActive, search, page = 1, limit = 20 } = filters;

    const where: FindOptionsWhere<ReportDefinition>[] = [];
    const baseWhere: FindOptionsWhere<ReportDefinition> = { tenantId };

    if (reportType) {
      baseWhere.reportType = reportType;
    }

    if (category) {
      baseWhere.category = category;
    }

    if (isPublic !== undefined) {
      baseWhere.isPublic = isPublic;
    }

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

    if (search) {
      where.push(
        { ...baseWhere, name: ILike(`%${search}%`) },
        { ...baseWhere, code: ILike(`%${search}%`) },
        { ...baseWhere, description: ILike(`%${search}%`) }
      );
    } else {
      where.push(baseWhere);
    }

    const [data, total] = await this.definitionRepository.findAndCount({
      where: where.length > 0 ? where : undefined,
      order: { category: 'ASC', name: 'ASC' },
      skip: (page - 1) * limit,
      take: limit,
    });

    return { data, total };
  }

  /**
   * Get a report definition by ID
   */
  async findDefinitionById(id: string, tenantId: string): Promise<ReportDefinition> {
    const definition = await this.definitionRepository.findOne({
      where: { id, tenantId },
    });

    if (!definition) {
      throw new NotFoundError('Report definition not found');
    }

    return definition;
  }

  /**
   * Get a report definition by code
   */
  async findDefinitionByCode(code: string, tenantId: string): Promise<ReportDefinition | null> {
    return this.definitionRepository.findOne({
      where: { code, tenantId },
    });
  }

  /**
   * Create a new report definition
   */
  async createDefinition(
    dto: CreateReportDefinitionDto,
    tenantId: string,
    createdBy: string
  ): Promise<ReportDefinition> {
    const existing = await this.findDefinitionByCode(dto.code, tenantId);
    if (existing) {
      throw new ConflictError(`Report definition with code '${dto.code}' already exists`);
    }

    const definition = this.definitionRepository.create({
      ...dto,
      tenantId,
      createdBy,
    });

    const saved = await this.definitionRepository.save(definition);
    logger.info('Report definition created', { id: saved.id, code: dto.code, tenantId });

    return saved;
  }

  /**
   * Update a report definition
   */
  async updateDefinition(
    id: string,
    dto: UpdateReportDefinitionDto,
    tenantId: string
  ): Promise<ReportDefinition> {
    const definition = await this.findDefinitionById(id, tenantId);

    Object.assign(definition, dto);

    const updated = await this.definitionRepository.save(definition);
    logger.info('Report definition updated', { id, tenantId });

    return updated;
  }

  /**
   * Delete a report definition (soft delete by setting isActive = false)
   */
  async deleteDefinition(id: string, tenantId: string): Promise<void> {
    const definition = await this.findDefinitionById(id, tenantId);
    definition.isActive = false;
    await this.definitionRepository.save(definition);
    logger.info('Report definition deactivated', { id, tenantId });
  }

  /**
   * Get all unique categories
   */
  async getCategories(tenantId: string): Promise<string[]> {
    const result = await this.definitionRepository
      .createQueryBuilder('def')
      .select('DISTINCT def.category', 'category')
      .where('def.tenant_id = :tenantId', { tenantId })
      .andWhere('def.category IS NOT NULL')
      .andWhere('def.is_active = true')
      .orderBy('def.category', 'ASC')
      .getRawMany();

    return result.map((r: { category: string }) => r.category);
  }

  // ==================== REPORT SCHEDULES ====================

  /**
   * Get all report schedules with optional filtering
   */
  async findAllSchedules(
    tenantId: string,
    filters: ReportScheduleFiltersDto = {}
  ): Promise<{ data: ReportSchedule[]; total: number }> {
    const { reportDefinitionId, isActive, search, page = 1, limit = 20 } = filters;

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

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

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

    if (search) {
      where.push({ ...baseWhere, name: ILike(`%${search}%`) });
    } else {
      where.push(baseWhere);
    }

    const [data, total] = await this.scheduleRepository.findAndCount({
      where: where.length > 0 ? where : undefined,
      relations: ['reportDefinition'],
      order: { name: 'ASC' },
      skip: (page - 1) * limit,
      take: limit,
    });

    return { data, total };
  }

  /**
   * Get a report schedule by ID
   */
  async findScheduleById(id: string, tenantId: string): Promise<ReportSchedule> {
    const schedule = await this.scheduleRepository.findOne({
      where: { id, tenantId },
      relations: ['reportDefinition', 'recipients'],
    });

    if (!schedule) {
      throw new NotFoundError('Report schedule not found');
    }

    return schedule;
  }

  /**
   * Create a new report schedule
   */
  async createSchedule(
    dto: CreateReportScheduleDto,
    tenantId: string,
    createdBy: string
  ): Promise<ReportSchedule> {
    // Verify definition exists
    await this.findDefinitionById(dto.reportDefinitionId, tenantId);

    const schedule = this.scheduleRepository.create({
      ...dto,
      tenantId,
      createdBy,
    });

    const saved = await this.scheduleRepository.save(schedule);
    logger.info('Report schedule created', { id: saved.id, name: dto.name, tenantId });

    return saved;
  }

  /**
   * Update a report schedule
   */
  async updateSchedule(
    id: string,
    dto: UpdateReportScheduleDto,
    tenantId: string
  ): Promise<ReportSchedule> {
    const schedule = await this.findScheduleById(id, tenantId);

    Object.assign(schedule, dto);

    const updated = await this.scheduleRepository.save(schedule);
    logger.info('Report schedule updated', { id, tenantId });

    return updated;
  }

  /**
   * Toggle schedule active status
   */
  async toggleSchedule(id: string, tenantId: string, isActive: boolean): Promise<ReportSchedule> {
    const schedule = await this.findScheduleById(id, tenantId);
    schedule.isActive = isActive;

    const updated = await this.scheduleRepository.save(schedule);
    logger.info('Report schedule toggled', { id, isActive, tenantId });

    return updated;
  }

  /**
   * Delete a report schedule
   */
  async deleteSchedule(id: string, tenantId: string): Promise<void> {
    const schedule = await this.findScheduleById(id, tenantId);
    await this.scheduleRepository.remove(schedule);
    logger.info('Report schedule deleted', { id, tenantId });
  }

  /**
   * Get schedules due for execution
   */
  async getDueSchedules(): Promise<ReportSchedule[]> {
    return this.scheduleRepository
      .createQueryBuilder('schedule')
      .where('schedule.is_active = true')
      .andWhere('schedule.next_run_at <= NOW()')
      .leftJoinAndSelect('schedule.reportDefinition', 'definition')
      .leftJoinAndSelect('schedule.recipients', 'recipients')
      .getMany();
  }

  // ==================== REPORT RECIPIENTS ====================

  /**
   * Get recipients for a schedule
   */
  async findRecipientsBySchedule(scheduleId: string): Promise<ReportRecipient[]> {
    return this.recipientRepository.find({
      where: { scheduleId },
      order: { createdAt: 'ASC' },
    });
  }

  /**
   * Add a recipient to a schedule
   */
  async addRecipient(
    dto: CreateReportRecipientDto,
    tenantId: string
  ): Promise<ReportRecipient> {
    // Verify schedule exists and belongs to tenant
    await this.findScheduleById(dto.scheduleId, tenantId);

    if (!dto.userId && !dto.email) {
      throw new ValidationError('Either userId or email is required');
    }

    const recipient = this.recipientRepository.create(dto);
    const saved = await this.recipientRepository.save(recipient);
    logger.info('Report recipient added', { id: saved.id, scheduleId: dto.scheduleId });

    return saved;
  }

  /**
   * Update a recipient
   */
  async updateRecipient(
    id: string,
    dto: UpdateReportRecipientDto
  ): Promise<ReportRecipient> {
    const recipient = await this.recipientRepository.findOne({ where: { id } });

    if (!recipient) {
      throw new NotFoundError('Report recipient not found');
    }

    Object.assign(recipient, dto);

    const updated = await this.recipientRepository.save(recipient);
    logger.info('Report recipient updated', { id });

    return updated;
  }

  /**
   * Remove a recipient
   */
  async removeRecipient(id: string): Promise<void> {
    const recipient = await this.recipientRepository.findOne({ where: { id } });

    if (!recipient) {
      throw new NotFoundError('Report recipient not found');
    }

    await this.recipientRepository.remove(recipient);
    logger.info('Report recipient removed', { id });
  }

  // ==================== CUSTOM REPORTS ====================

  /**
   * Get all custom reports for a user
   */
  async findAllCustomReports(
    tenantId: string,
    ownerId: string,
    filters: CustomReportFiltersDto = {}
  ): Promise<{ data: CustomReport[]; total: number }> {
    const { baseDefinitionId, isFavorite, search, page = 1, limit = 20 } = filters;

    const where: FindOptionsWhere<CustomReport>[] = [];
    const baseWhere: FindOptionsWhere<CustomReport> = { tenantId, ownerId };

    if (baseDefinitionId) {
      baseWhere.baseDefinitionId = baseDefinitionId;
    }

    if (isFavorite !== undefined) {
      baseWhere.isFavorite = isFavorite;
    }

    if (search) {
      where.push(
        { ...baseWhere, name: ILike(`%${search}%`) },
        { ...baseWhere, description: ILike(`%${search}%`) }
      );
    } else {
      where.push(baseWhere);
    }

    const [data, total] = await this.customReportRepository.findAndCount({
      where: where.length > 0 ? where : undefined,
      relations: ['baseDefinition'],
      order: { isFavorite: 'DESC', name: 'ASC' },
      skip: (page - 1) * limit,
      take: limit,
    });

    return { data, total };
  }

  /**
   * Get a custom report by ID
   */
  async findCustomReportById(id: string, tenantId: string, ownerId: string): Promise<CustomReport> {
    const customReport = await this.customReportRepository.findOne({
      where: { id, tenantId, ownerId },
      relations: ['baseDefinition'],
    });

    if (!customReport) {
      throw new NotFoundError('Custom report not found');
    }

    return customReport;
  }

  /**
   * Create a custom report
   */
  async createCustomReport(
    dto: CreateCustomReportDto,
    tenantId: string,
    ownerId: string
  ): Promise<CustomReport> {
    if (dto.baseDefinitionId) {
      await this.findDefinitionById(dto.baseDefinitionId, tenantId);
    }

    const customReport = this.customReportRepository.create({
      ...dto,
      tenantId,
      ownerId,
    });

    const saved = await this.customReportRepository.save(customReport);
    logger.info('Custom report created', { id: saved.id, name: dto.name, tenantId, ownerId });

    return saved;
  }

  /**
   * Update a custom report
   */
  async updateCustomReport(
    id: string,
    dto: UpdateCustomReportDto,
    tenantId: string,
    ownerId: string
  ): Promise<CustomReport> {
    const customReport = await this.findCustomReportById(id, tenantId, ownerId);

    if (dto.baseDefinitionId) {
      await this.findDefinitionById(dto.baseDefinitionId, tenantId);
    }

    Object.assign(customReport, dto);

    const updated = await this.customReportRepository.save(customReport);
    logger.info('Custom report updated', { id, tenantId, ownerId });

    return updated;
  }

  /**
   * Toggle favorite status
   */
  async toggleFavorite(id: string, tenantId: string, ownerId: string): Promise<CustomReport> {
    const customReport = await this.findCustomReportById(id, tenantId, ownerId);
    customReport.isFavorite = !customReport.isFavorite;

    const updated = await this.customReportRepository.save(customReport);
    logger.info('Custom report favorite toggled', { id, isFavorite: customReport.isFavorite });

    return updated;
  }

  /**
   * Delete a custom report
   */
  async deleteCustomReport(id: string, tenantId: string, ownerId: string): Promise<void> {
    const customReport = await this.findCustomReportById(id, tenantId, ownerId);
    await this.customReportRepository.remove(customReport);
    logger.info('Custom report deleted', { id, tenantId, ownerId });
  }
}
