import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { PrismaService } from '../../prisma/prisma.service';
import { AssignClientDto } from './dto/assign-client.dto';
import { MoveStageDto } from './dto/move-stage.dto';
import { TransferClientDto } from './dto/transfer-client.dto';
import { RefuseClientDto } from './dto/refuse-client.dto';
import { FindFunnelDto } from './dto/find-funnel.dto';
import { FunnelStage, Prisma } from '@prisma/client';

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

export interface ConversionStats {
  newToAssigned: number;
  assignedToCompleted: number;
  completedToRegular: number;
  totalNew: number;
  totalAssigned: number;
  totalCompleted: number;
  totalRegular: number;
}

export interface StageDistribution {
  stage: FunnelStage;
  count: number;
}

@Injectable()
export class FunnelService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly eventEmitter: EventEmitter2,
  ) {}

  /**
   * List funnel entries by stage with cursor-based pagination.
   * Includes client and trainer relations.
   */
  async findByStage(
    clubId: string,
    params: FindFunnelDto & { trainerFilter?: string },
  ): Promise<PaginatedResult<any>> {
    const { cursor, limit = 20, stage, trainerId, trainerFilter } = params;

    const where: Prisma.FunnelEntryWhereInput = {
      clubId,
      ...(stage ? { stage } : {}),
      ...(trainerFilter ? { trainerId: trainerFilter } : {}),
      ...(trainerId && !trainerFilter ? { trainerId } : {}),
    };

    const entries = await this.prisma.funnelEntry.findMany({
      where,
      take: limit + 1,
      ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
      orderBy: { createdAt: 'desc' },
      include: {
        client: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            phone: true,
            avatarUrl: true,
          },
        },
        trainer: {
          select: { id: true, firstName: true, lastName: true },
        },
      },
    });

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

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

  /**
   * Get the full funnel transition history for a specific client.
   */
  async findByClient(clubId: string, clientId: string) {
    await this.ensureClientExists(clubId, clientId);

    return this.prisma.funnelEntry.findMany({
      where: { clubId, clientId },
      orderBy: { createdAt: 'desc' },
      include: {
        client: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            phone: true,
            avatarUrl: true,
          },
        },
        trainer: {
          select: { id: true, firstName: true, lastName: true },
        },
      },
    });
  }

  /**
   * Assign a client to a trainer: create ASSIGNED funnel entry
   * and update client.assignedTrainerId.
   */
  async assignClient(clubId: string, dto: AssignClientDto, assignedById: string) {
    await this.ensureClientExists(clubId, dto.clientId);

    const lastEntry = await this.getLastEntry(clubId, dto.clientId);

    const [entry] = await this.prisma.$transaction([
      this.prisma.funnelEntry.create({
        data: {
          clubId,
          clientId: dto.clientId,
          trainerId: dto.trainerId,
          stage: FunnelStage.ASSIGNED,
          previousStage: lastEntry?.stage ?? null,
          comment: dto.comment,
        },
        include: {
          client: {
            select: {
              id: true,
              firstName: true,
              lastName: true,
              phone: true,
            },
          },
          trainer: {
            select: { id: true, firstName: true, lastName: true },
          },
        },
      }),
      this.prisma.client.update({
        where: { id: dto.clientId },
        data: { assignedTrainerId: dto.trainerId },
      }),
      // Remove old NEW entries so the client no longer appears in the unassigned list
      this.prisma.funnelEntry.deleteMany({
        where: { clubId, clientId: dto.clientId, stage: FunnelStage.NEW },
      }),
    ]);

    this.eventEmitter.emit('funnel.assigned', { clubId, data: entry });

    return entry;
  }

  /**
   * Move a client to a new funnel stage. Records the previous stage
   * from the last entry.
   */
  async moveStage(clubId: string, clientId: string, dto: MoveStageDto) {
    await this.ensureClientExists(clubId, clientId);

    const lastEntry = await this.getLastEntry(clubId, clientId);
    if (!lastEntry) {
      throw new BadRequestException(
        'Client has no funnel history. Assign the client first.',
      );
    }

    if (lastEntry.stage === dto.stage) {
      throw new BadRequestException(
        `Client is already at stage ${dto.stage}`,
      );
    }

    const [entry] = await this.prisma.$transaction([
      this.prisma.funnelEntry.create({
        data: {
          clubId,
          clientId,
          trainerId: lastEntry.trainerId,
          stage: dto.stage,
          previousStage: lastEntry.stage,
          comment: dto.comment,
        },
        include: {
          client: {
            select: {
              id: true,
              firstName: true,
              lastName: true,
              phone: true,
            },
          },
          trainer: {
            select: { id: true, firstName: true, lastName: true },
          },
        },
      }),
      // Remove old entries with the previous stage so client appears only at the new stage
      this.prisma.funnelEntry.deleteMany({
        where: { clubId, clientId, stage: lastEntry.stage },
      }),
    ]);

    this.eventEmitter.emit('funnel.stage_changed', { clubId, data: entry });

    return entry;
  }

  /**
   * Transfer a client to another department. Creates a TRANSFERRED
   * funnel entry and updates the trainer's departmentId on the client
   * record (via assignedTrainerId reset).
   */
  async transferClient(
    clubId: string,
    dto: TransferClientDto,
    transferredById: string,
  ) {
    await this.ensureClientExists(clubId, dto.clientId);

    const lastEntry = await this.getLastEntry(clubId, dto.clientId);

    const entry = await this.prisma.funnelEntry.create({
      data: {
        clubId,
        clientId: dto.clientId,
        trainerId: null,
        stage: FunnelStage.TRANSFERRED,
        previousStage: lastEntry?.stage ?? null,
        comment: dto.comment,
      },
      include: {
        client: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            phone: true,
          },
        },
        trainer: {
          select: { id: true, firstName: true, lastName: true },
        },
      },
    });

    // Reset the assigned trainer since the client is transferred
    await this.prisma.client.update({
      where: { id: dto.clientId },
      data: { assignedTrainerId: null },
    });

    return entry;
  }

  /**
   * Refuse a client with a mandatory comment. Creates a REFUSED
   * funnel entry.
   */
  async refuseClient(
    clubId: string,
    clientId: string,
    dto: RefuseClientDto,
  ) {
    await this.ensureClientExists(clubId, clientId);

    const lastEntry = await this.getLastEntry(clubId, clientId);

    return this.prisma.funnelEntry.create({
      data: {
        clubId,
        clientId,
        trainerId: lastEntry?.trainerId ?? null,
        stage: FunnelStage.REFUSED,
        previousStage: lastEntry?.stage ?? null,
        comment: dto.comment,
      },
      include: {
        client: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            phone: true,
          },
        },
        trainer: {
          select: { id: true, firstName: true, lastName: true },
        },
      },
    });
  }

  /**
   * Calculate conversion rates between funnel stages.
   * Optionally filtered by date range.
   */
  async getConversionStats(
    clubId: string,
    dateFrom?: string,
    dateTo?: string,
  ): Promise<ConversionStats> {
    const dateFilter: Prisma.FunnelEntryWhereInput = {
      clubId,
      ...(dateFrom || dateTo
        ? {
            createdAt: {
              ...(dateFrom ? { gte: new Date(dateFrom) } : {}),
              ...(dateTo ? { lte: new Date(dateTo) } : {}),
            },
          }
        : {}),
    };

    const [totalNew, totalAssigned, totalCompleted, totalRegular] =
      await Promise.all([
        this.prisma.funnelEntry.count({
          where: { ...dateFilter, stage: FunnelStage.NEW },
        }),
        this.prisma.funnelEntry.count({
          where: { ...dateFilter, stage: FunnelStage.ASSIGNED },
        }),
        this.prisma.funnelEntry.count({
          where: { ...dateFilter, stage: FunnelStage.COMPLETED },
        }),
        this.prisma.funnelEntry.count({
          where: { ...dateFilter, stage: FunnelStage.REGULAR },
        }),
      ]);

    const safeDiv = (numerator: number, denominator: number): number =>
      denominator === 0 ? 0 : Math.round((numerator / denominator) * 10000) / 100;

    return {
      newToAssigned: safeDiv(totalAssigned, totalNew),
      assignedToCompleted: safeDiv(totalCompleted, totalAssigned),
      completedToRegular: safeDiv(totalRegular, totalCompleted),
      totalNew,
      totalAssigned,
      totalCompleted,
      totalRegular,
    };
  }

  /**
   * Count funnel entries per stage (for the funnel distribution chart).
   */
  async getDistribution(clubId: string): Promise<StageDistribution[]> {
    const results = await this.prisma.funnelEntry.groupBy({
      by: ['stage'],
      where: { clubId },
      _count: { stage: true },
    });

    return results.map((r) => ({
      stage: r.stage,
      count: r._count.stage,
    }));
  }

  /**
   * Get the most recent funnel entry for a client.
   */
  private async getLastEntry(clubId: string, clientId: string) {
    return this.prisma.funnelEntry.findFirst({
      where: { clubId, clientId },
      orderBy: { createdAt: 'desc' },
    });
  }

  /**
   * Ensure a client exists within the given club, or throw NotFoundException.
   */
  private async ensureClientExists(
    clubId: string,
    clientId: string,
  ): Promise<void> {
    const count = await this.prisma.client.count({
      where: { id: clientId, clubId },
    });

    if (count === 0) {
      throw new NotFoundException(`Client with id ${clientId} not found`);
    }
  }
}
