import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { GenerateReportDto, ReportType } from './dto/generate-report.dto';
import { FindReportsDto } from './dto/find-reports.dto';
import { Prisma, ReportStatus } from '@prisma/client';
import { StatsService } from '../stats/stats.service';
import * as fs from 'fs';
import * as path from 'path';
import {
  generateClientsSummaryPdf,
  generateFunnelConversionPdf,
  generateSalesSummaryPdf,
  generateSleepingClientsPdf,
  generateTrainerPerformancePdf,
} from './pdf-generator';

export interface PaginatedResult<T> {
  data: T[];
  nextCursor: string | null;
  hasMore: boolean;
}

const REPORT_INCLUDE = {
  generatedBy: {
    select: { id: true, firstName: true, lastName: true },
  },
};

const REPORTS_DIR = path.join(process.cwd(), 'storage', 'reports');

@Injectable()
export class ReportsService {
  private readonly logger = new Logger(ReportsService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly statsService: StatsService,
  ) {}

  async findAll(
    clubId: string,
    params: FindReportsDto,
  ): Promise<PaginatedResult<any>> {
    const { cursor, limit = 20, status, type } = params;

    const where: Prisma.ReportWhereInput = {
      clubId,
      ...(status ? { status } : {}),
      ...(type ? { type } : {}),
    };

    const items = await this.prisma.report.findMany({
      where,
      take: limit + 1,
      ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
      orderBy: { createdAt: 'desc' },
      include: REPORT_INCLUDE,
    });

    const hasMore = items.length > limit;
    const data = hasMore ? items.slice(0, -1) : items;

    return {
      data,
      nextCursor: hasMore ? data[data.length - 1].id : null,
      hasMore,
    };
  }

  async findById(clubId: string, id: string) {
    const report = await this.prisma.report.findFirst({
      where: { id, clubId },
      include: REPORT_INCLUDE,
    });

    if (!report) {
      throw new NotFoundException(`Report with id ${id} not found`);
    }
    return report;
  }

  async generate(clubId: string, userId: string, dto: GenerateReportDto) {
    const report = await this.prisma.report.create({
      data: {
        clubId,
        generatedById: userId,
        type: dto.type,
        title: dto.title,
        parametersJson: dto.parameters ?? {},
        status: ReportStatus.PENDING,
      },
      include: REPORT_INCLUDE,
    });

    // Generate PDF asynchronously (fire-and-forget)
    this.generatePdfAsync(report.id, clubId, userId, dto).catch((err) => {
      this.logger.error(`PDF generation failed for report ${report.id}: ${err.message}`);
    });

    return { ...report, status: ReportStatus.GENERATING };
  }

  async getDownloadUrl(clubId: string, id: string) {
    const report = await this.prisma.report.findFirst({
      where: { id, clubId },
    });

    if (!report) {
      throw new NotFoundException(`Report with id ${id} not found`);
    }

    if (report.status !== ReportStatus.COMPLETED || !report.fileUrl) {
      throw new NotFoundException('Report file is not available yet');
    }

    return { fileUrl: report.fileUrl };
  }

  private async generatePdfAsync(
    reportId: string,
    clubId: string,
    userId: string,
    dto: GenerateReportDto,
  ): Promise<void> {
    await this.prisma.report.update({
      where: { id: reportId },
      data: { status: ReportStatus.GENERATING },
    });

    try {
      const params = dto.parameters ?? {};
      const club = await this.prisma.club.findUnique({ where: { id: clubId } });
      const user = await this.prisma.user.findUnique({ where: { id: userId } });
      const meta = {
        title: dto.title,
        clubName: club?.name ?? 'Клуб',
        generatedBy: user ? `${user.firstName} ${user.lastName}` : 'Система',
        dateFrom: params.dateFrom,
        dateTo: params.dateTo,
      };

      let pdfDoc: PDFKit.PDFDocument;

      switch (dto.type) {
        case ReportType.CLIENTS_SUMMARY: {
          const summary = await this.statsService.getSummary(clubId, undefined, params.dateFrom, params.dateTo);
          const rawClients = await this.prisma.client.findMany({
            where: { clubId },
            take: 500,
            orderBy: { createdAt: 'desc' },
            select: { firstName: true, lastName: true, phone: true, source: true, createdAt: true },
          });
          const clients = rawClients.map((c) => ({
            firstName: c.firstName,
            lastName: c.lastName,
            phone: c.phone,
            status: c.source ?? '—',
            createdAt: c.createdAt.toISOString(),
          }));
          pdfDoc = generateClientsSummaryPdf(meta, summary, clients);
          break;
        }

        case ReportType.FUNNEL_CONVERSION: {
          const funnel = await this.statsService.getFunnelConversion(clubId, undefined, params.dateFrom, params.dateTo);
          pdfDoc = generateFunnelConversionPdf(meta, funnel);
          break;
        }

        case ReportType.SALES_SUMMARY: {
          const summary = await this.statsService.getSummary(clubId, undefined, params.dateFrom, params.dateTo);
          const salesByTrainer = await this.prisma.$queryRaw<
            { trainerName: string; count: number; totalAmount: number; paidAmount: number }[]
          >`
            SELECT
              CONCAT(u."firstName", ' ', u."lastName") as "trainerName",
              COUNT(s.id)::int as count,
              COALESCE(SUM(s.amount), 0)::float as "totalAmount",
              COALESCE(SUM(CASE WHEN s.status = 'PAID' THEN s.amount ELSE 0 END), 0)::float as "paidAmount"
            FROM sales s
            JOIN users u ON s."trainerId" = u.id
            WHERE s."clubId" = ${clubId}
            GROUP BY u.id, u."firstName", u."lastName"
            ORDER BY "totalAmount" DESC
          `;
          pdfDoc = generateSalesSummaryPdf(meta, summary, salesByTrainer);
          break;
        }

        case ReportType.SLEEPING_CLIENTS: {
          const sleepingRaw = await this.prisma.$queryRaw<
            { firstName: string; lastName: string; phone: string; lastActivity: string; trainerName: string }[]
          >`
            SELECT
              c."firstName", c."lastName", c.phone,
              c."lastActivityAt" as "lastActivity",
              COALESCE(CONCAT(u."firstName", ' ', u."lastName"), '') as "trainerName"
            FROM clients c
            LEFT JOIN users u ON c."assignedTrainerId" = u.id
            WHERE c."clubId" = ${clubId}
              AND c."lastActivityAt" < NOW() - INTERVAL '30 days'
              AND (
                EXISTS (
                  SELECT 1 FROM client_memberships cm
                  WHERE cm."clientId" = c.id AND cm."isActive" = true
                )
                OR EXISTS (
                  SELECT 1 FROM client_services cs
                  WHERE cs."clientId" = c.id AND cs.status = 'ACTIVE'
                )
              )
            ORDER BY c."lastActivityAt" ASC
            LIMIT 500
          `;
          pdfDoc = generateSleepingClientsPdf(meta, sleepingRaw);
          break;
        }

        case ReportType.TRAINER_PERFORMANCE: {
          const trainerId = params.trainerId;
          const trainer = trainerId
            ? await this.prisma.user.findUnique({
                where: { id: trainerId },
                select: { firstName: true, lastName: true },
              })
            : null;
          const trainerStats = trainerId
            ? await this.statsService.getTrainerStats(clubId, trainerId, params.dateFrom, params.dateTo)
            : null;

          pdfDoc = generateTrainerPerformancePdf(
            meta,
            trainer ?? { firstName: 'Неизвестно', lastName: '' },
            {
              trainings: {
                total: trainerStats?.trainings.total ?? 0,
                completed: trainerStats?.trainings.completed ?? 0,
                cancelled: trainerStats?.trainings.cancelled ?? 0,
              },
              sales: {
                totalAmount: trainerStats?.sales.totalAmount ?? 0,
                paidAmount: trainerStats?.sales.paidAmount ?? 0,
                count: trainerStats?.sales.count ?? 0,
              },
              clients: trainerStats?.clientCount ?? 0,
              funnel: {
                newToAssigned: trainerStats?.funnel.newToAssigned ?? 0,
                assignedToCompleted: trainerStats?.funnel.assignedToCompleted ?? 0,
                completedToRegular: trainerStats?.funnel.completedToRegular ?? 0,
              },
            },
          );
          break;
        }

        default:
          throw new Error(`Unknown report type: ${dto.type}`);
      }

      // Save PDF to disk
      if (!fs.existsSync(REPORTS_DIR)) {
        fs.mkdirSync(REPORTS_DIR, { recursive: true });
      }
      const fileName = `${reportId}.pdf`;
      const filePath = path.join(REPORTS_DIR, fileName);

      await new Promise<void>((resolve, reject) => {
        const writeStream = fs.createWriteStream(filePath);
        pdfDoc.pipe(writeStream);
        writeStream.on('finish', resolve);
        writeStream.on('error', reject);
      });

      const fileUrl = `/v1/reports/${reportId}/file`;

      await this.prisma.report.update({
        where: { id: reportId },
        data: {
          status: ReportStatus.COMPLETED,
          fileUrl,
          generatedAt: new Date(),
        },
      });

      this.logger.log(`Report ${reportId} generated: ${filePath}`);
    } catch (error: any) {
      this.logger.error(`Report ${reportId} failed: ${error.message}`);
      await this.prisma.report.update({
        where: { id: reportId },
        data: { status: ReportStatus.FAILED },
      });
    }
  }

  async getFile(clubId: string, id: string): Promise<string> {
    const report = await this.prisma.report.findFirst({
      where: { id, clubId, status: ReportStatus.COMPLETED },
    });

    if (!report) {
      throw new NotFoundException('Report not found or not ready');
    }

    const filePath = path.resolve(REPORTS_DIR, `${id}.pdf`);
    const normalizedDir = path.resolve(REPORTS_DIR);
    if (!filePath.startsWith(normalizedDir)) {
      throw new ForbiddenException('Invalid file path');
    }
    if (!fs.existsSync(filePath)) {
      throw new NotFoundException('Report file not found on disk');
    }

    return filePath;
  }
}
