import {
  Injectable,
  NotFoundException,
  ConflictException,
  BadRequestException,
} from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { Prisma, S3StatusType, S3Type } from '@prisma/client';
import {
  CreateS3PlanDto,
  AddS3CategoryDto,
  UpdateS3StatusDto,
  UpdateS3CategoryDto,
  UpdateS3PlanDto,
  QueryS3PlansDto,
  QueryS3DashboardDto,
  QueryS3StatisticsDto,
} from './dto';

/**
 * Service for managing 3S (Seiri, Seiton, Seiso) planning
 * Implements LEAN methodology for workplace organization
 */
@Injectable()
export class S3PlanningService {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * Create a new 3S plan for a department
   */
  async createPlan(dto: CreateS3PlanDto, userId: string) {
    // Validate department exists
    const department = await this.prisma.department.findUnique({
      where: { id: dto.departmentId },
    });

    if (!department) {
      throw new NotFoundException(
        `Department with ID ${dto.departmentId} not found`,
      );
    }

    // Check for existing plan
    const existingPlan = await this.prisma.s3Plan.findUnique({
      where: {
        departmentId_year_month: {
          departmentId: dto.departmentId,
          year: dto.year,
          month: dto.month,
        },
      },
    });

    if (existingPlan) {
      throw new ConflictException(
        `Plan for ${dto.month}/${dto.year} already exists for this department`,
      );
    }

    // Create plan with optional categories
    const plan = await this.prisma.s3Plan.create({
      data: {
        departmentId: dto.departmentId,
        year: dto.year,
        month: dto.month,
        isActive: dto.isActive ?? true,
        createdById: userId,
        categories: dto.categories
          ? {
              create: dto.categories.map((cat, index) => ({
                name: cat.name,
                s3Type: cat.s3Type,
                sortOrder: cat.sortOrder ?? index,
              })),
            }
          : undefined,
      },
      include: {
        department: true,
        createdBy: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            email: true,
          },
        },
        categories: {
          orderBy: { sortOrder: 'asc' },
          include: {
            statuses: true,
          },
        },
      },
    });

    // Initialize status entries for all weeks if categories exist
    if (plan.categories.length > 0) {
      const weeks = this.getWeeksForMonth(dto.year, dto.month);
      const statusData: Prisma.S3StatusCreateManyInput[] = [];

      for (const category of plan.categories) {
        for (const week of weeks) {
          statusData.push({
            categoryId: category.id,
            week: week,
            status: S3StatusType.NOT_APPLICABLE,
          });
        }
      }

      await this.prisma.s3Status.createMany({ data: statusData });
    }

    // Return updated plan with statuses
    return this.findOne(plan.id);
  }

  /**
   * Get all plans with filtering and pagination
   */
  async findAll(query: QueryS3PlansDto) {
    const {
      page = 1,
      limit = 20,
      departmentId,
      year,
      month,
      isActive,
      sortBy = 'createdAt',
      sortOrder = 'desc',
    } = query;
    const skip = (page - 1) * limit;

    const where: Prisma.S3PlanWhereInput = {
      ...(departmentId && { departmentId }),
      ...(year && { year }),
      ...(month && { month }),
      ...(isActive !== undefined && { isActive }),
    };

    const orderBy: Prisma.S3PlanOrderByWithRelationInput = {
      [sortBy]: sortOrder,
    };

    const [plans, total] = await Promise.all([
      this.prisma.s3Plan.findMany({
        where,
        skip,
        take: limit,
        orderBy,
        include: {
          department: true,
          createdBy: {
            select: {
              id: true,
              firstName: true,
              lastName: true,
              email: true,
            },
          },
          _count: {
            select: {
              categories: true,
            },
          },
        },
      }),
      this.prisma.s3Plan.count({ where }),
    ]);

    // Calculate progress for each plan
    const plansWithProgress = await Promise.all(
      plans.map(async (plan) => {
        const progress = await this.calculatePlanProgress(plan.id);
        return { ...plan, progress };
      }),
    );

    return {
      data: plansWithProgress,
      meta: {
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  /**
   * Get a single plan by ID with all related data
   */
  async findOne(id: string) {
    const plan = await this.prisma.s3Plan.findUnique({
      where: { id },
      include: {
        department: true,
        createdBy: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            email: true,
          },
        },
        categories: {
          orderBy: { sortOrder: 'asc' },
          include: {
            statuses: {
              orderBy: { week: 'asc' },
              include: {
                completedBy: {
                  select: {
                    id: true,
                    firstName: true,
                    lastName: true,
                  },
                },
              },
            },
          },
        },
      },
    });

    if (!plan) {
      throw new NotFoundException(`Plan with ID ${id} not found`);
    }

    const progress = await this.calculatePlanProgress(id);
    return { ...plan, progress };
  }

  /**
   * Update a plan
   */
  async updatePlan(id: string, dto: UpdateS3PlanDto) {
    await this.findOne(id);

    return this.prisma.s3Plan.update({
      where: { id },
      data: dto,
      include: {
        department: true,
        categories: {
          orderBy: { sortOrder: 'asc' },
        },
      },
    });
  }

  /**
   * Delete a plan
   */
  async deletePlan(id: string) {
    await this.findOne(id);

    return this.prisma.s3Plan.delete({
      where: { id },
    });
  }

  /**
   * Add a category to an existing plan
   */
  async addCategory(planId: string, dto: AddS3CategoryDto) {
    const plan = await this.findOne(planId);

    // Get max sort order
    const maxSortOrder = await this.prisma.s3Category.aggregate({
      where: { planId },
      _max: { sortOrder: true },
    });

    const category = await this.prisma.s3Category.create({
      data: {
        planId,
        name: dto.name,
        s3Type: dto.s3Type,
        sortOrder: dto.sortOrder ?? (maxSortOrder._max.sortOrder ?? 0) + 1,
      },
    });

    // Initialize status entries for all weeks
    const weeks = this.getWeeksForMonth(plan.year, plan.month);
    const statusData: Prisma.S3StatusCreateManyInput[] = weeks.map((week) => ({
      categoryId: category.id,
      week,
      status: S3StatusType.NOT_APPLICABLE,
    }));

    await this.prisma.s3Status.createMany({ data: statusData });

    return this.prisma.s3Category.findUnique({
      where: { id: category.id },
      include: {
        statuses: {
          orderBy: { week: 'asc' },
        },
      },
    });
  }

  /**
   * Update a category
   */
  async updateCategory(id: string, dto: UpdateS3CategoryDto) {
    const category = await this.prisma.s3Category.findUnique({
      where: { id },
    });

    if (!category) {
      throw new NotFoundException(`Category with ID ${id} not found`);
    }

    return this.prisma.s3Category.update({
      where: { id },
      data: dto,
      include: {
        statuses: {
          orderBy: { week: 'asc' },
        },
      },
    });
  }

  /**
   * Delete a category
   */
  async deleteCategory(id: string) {
    const category = await this.prisma.s3Category.findUnique({
      where: { id },
    });

    if (!category) {
      throw new NotFoundException(`Category with ID ${id} not found`);
    }

    return this.prisma.s3Category.delete({
      where: { id },
    });
  }

  /**
   * Get categories for a plan
   */
  async getCategories(planId: string) {
    await this.findOne(planId);

    return this.prisma.s3Category.findMany({
      where: { planId },
      orderBy: { sortOrder: 'asc' },
      include: {
        statuses: {
          orderBy: { week: 'asc' },
          include: {
            completedBy: {
              select: {
                id: true,
                firstName: true,
                lastName: true,
              },
            },
          },
        },
      },
    });
  }

  /**
   * Update a status entry
   */
  async updateStatus(id: string, dto: UpdateS3StatusDto, userId: string) {
    const status = await this.prisma.s3Status.findUnique({
      where: { id },
    });

    if (!status) {
      throw new NotFoundException(`Status with ID ${id} not found`);
    }

    const isCompleting = dto.status === S3StatusType.GREEN;
    const wasCompleted = status.status === S3StatusType.GREEN;

    return this.prisma.s3Status.update({
      where: { id },
      data: {
        status: dto.status,
        photo: dto.photo,
        note: dto.note,
        completedAt:
          isCompleting && !wasCompleted
            ? new Date()
            : !isCompleting
              ? null
              : undefined,
        completedById:
          isCompleting && !wasCompleted ? userId : !isCompleting ? null : undefined,
      },
      include: {
        completedBy: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
          },
        },
        category: true,
      },
    });
  }

  /**
   * Upload photo for a status entry
   */
  async uploadStatusPhoto(id: string, photoPath: string, userId: string) {
    const status = await this.prisma.s3Status.findUnique({
      where: { id },
    });

    if (!status) {
      throw new NotFoundException(`Status with ID ${id} not found`);
    }

    return this.prisma.s3Status.update({
      where: { id },
      data: {
        photo: photoPath,
      },
    });
  }

  /**
   * Get dashboard data for a department
   */
  async getDashboard(departmentId: string, query: QueryS3DashboardDto) {
    // Validate department
    const department = await this.prisma.department.findUnique({
      where: { id: departmentId },
    });

    if (!department) {
      throw new NotFoundException(
        `Department with ID ${departmentId} not found`,
      );
    }

    const now = new Date();
    const year = query.year ?? now.getFullYear();
    const month = query.month ?? now.getMonth() + 1;

    // Get current plan
    const currentPlan = await this.prisma.s3Plan.findUnique({
      where: {
        departmentId_year_month: {
          departmentId,
          year,
          month,
        },
      },
      include: {
        categories: {
          orderBy: { sortOrder: 'asc' },
          include: {
            statuses: {
              orderBy: { week: 'asc' },
            },
          },
        },
      },
    });

    // Calculate statistics
    const stats = await this.calculateDepartmentStats(departmentId, year);

    return {
      department,
      year,
      month,
      currentPlan,
      stats,
    };
  }

  /**
   * Get overall statistics
   */
  async getStatistics(query: QueryS3StatisticsDto) {
    const year = query.year ?? new Date().getFullYear();

    const where: Prisma.S3PlanWhereInput = {
      year,
      ...(query.departmentIds && {
        departmentId: { in: query.departmentIds },
      }),
    };

    // Get all plans for the year
    const plans = await this.prisma.s3Plan.findMany({
      where,
      include: {
        department: true,
        categories: {
          include: {
            statuses: true,
          },
        },
      },
    });

    // Calculate overall statistics
    let totalStatuses = 0;
    let greenCount = 0;
    let yellowCount = 0;
    let redCount = 0;
    let notApplicableCount = 0;

    const byType: Record<S3Type, { total: number; completed: number }> = {
      [S3Type.SEIRI]: { total: 0, completed: 0 },
      [S3Type.SEITON]: { total: 0, completed: 0 },
      [S3Type.SEISO]: { total: 0, completed: 0 },
    };

    const byDepartment: Record<
      string,
      { name: string; total: number; completed: number }
    > = {};

    for (const plan of plans) {
      if (!byDepartment[plan.departmentId]) {
        byDepartment[plan.departmentId] = {
          name: plan.department.name,
          total: 0,
          completed: 0,
        };
      }

      for (const category of plan.categories) {
        byType[category.s3Type].total += category.statuses.length;

        for (const status of category.statuses) {
          totalStatuses++;
          byDepartment[plan.departmentId].total++;

          switch (status.status) {
            case S3StatusType.GREEN:
              greenCount++;
              byType[category.s3Type].completed++;
              byDepartment[plan.departmentId].completed++;
              break;
            case S3StatusType.YELLOW:
              yellowCount++;
              break;
            case S3StatusType.RED:
              redCount++;
              break;
            case S3StatusType.NOT_APPLICABLE:
              notApplicableCount++;
              break;
          }
        }
      }
    }

    const completionRate =
      totalStatuses > 0 ? Math.round((greenCount / totalStatuses) * 100) : 0;

    return {
      year,
      totalPlans: plans.length,
      totalStatuses,
      statusCounts: {
        green: greenCount,
        yellow: yellowCount,
        red: redCount,
        notApplicable: notApplicableCount,
      },
      completionRate,
      byType: Object.entries(byType).map(([type, data]) => ({
        type,
        ...data,
        completionRate:
          data.total > 0
            ? Math.round((data.completed / data.total) * 100)
            : 0,
      })),
      byDepartment: Object.entries(byDepartment).map(([id, data]) => ({
        departmentId: id,
        ...data,
        completionRate:
          data.total > 0
            ? Math.round((data.completed / data.total) * 100)
            : 0,
      })),
    };
  }

  /**
   * Calculate progress percentage for a plan
   */
  private async calculatePlanProgress(planId: string): Promise<number> {
    const result = await this.prisma.s3Status.groupBy({
      by: ['status'],
      where: {
        category: {
          planId,
        },
      },
      _count: true,
    });

    const total = result.reduce((sum, r) => sum + r._count, 0);
    const completed =
      result.find((r) => r.status === S3StatusType.GREEN)?._count || 0;

    return total > 0 ? Math.round((completed / total) * 100) : 0;
  }

  /**
   * Calculate department statistics for a year
   */
  private async calculateDepartmentStats(
    departmentId: string,
    year: number,
  ) {
    const plans = await this.prisma.s3Plan.findMany({
      where: { departmentId, year },
      include: {
        categories: {
          include: {
            statuses: true,
          },
        },
      },
    });

    let total = 0;
    let completed = 0;
    let inProgress = 0;
    let issues = 0;

    for (const plan of plans) {
      for (const category of plan.categories) {
        for (const status of category.statuses) {
          total++;
          switch (status.status) {
            case S3StatusType.GREEN:
              completed++;
              break;
            case S3StatusType.YELLOW:
              inProgress++;
              break;
            case S3StatusType.RED:
              issues++;
              break;
          }
        }
      }
    }

    return {
      totalPlans: plans.length,
      totalEntries: total,
      completed,
      inProgress,
      issues,
      completionRate: total > 0 ? Math.round((completed / total) * 100) : 0,
    };
  }

  /**
   * Get ISO weeks that fall within a given month
   */
  private getWeeksForMonth(year: number, month: number): number[] {
    const weeks: Set<number> = new Set();
    const firstDay = new Date(year, month - 1, 1);
    const lastDay = new Date(year, month, 0);

    for (
      let day = new Date(firstDay);
      day <= lastDay;
      day.setDate(day.getDate() + 1)
    ) {
      weeks.add(this.getISOWeek(day));
    }

    return Array.from(weeks).sort((a, b) => a - b);
  }

  /**
   * Get ISO week number for a date
   */
  private getISOWeek(date: Date): number {
    const d = new Date(
      Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
    );
    const dayNum = d.getUTCDay() || 7;
    d.setUTCDate(d.getUTCDate() + 4 - dayNum);
    const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
    return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
  }
}
