import {
  Injectable,
  NotFoundException,
  BadRequestException,
  ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Not, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
import { ClassroomRental, ClassroomRentalStatus } from '../entities/classroom-rental.entity';
import { Classroom, ClassroomStatus } from '../entities/classroom.entity';
import { Organization } from '../entities/organization.entity';
import { ClassSchedule } from '../entities/class-schedule.entity';
import { CreateRentalDto, UpdateRentalDto } from './dto/rental.dto';

import * as path from 'path';
import * as fs from 'fs';

// 预设色板（与 organizations.service 保持一致，作为颜色兜底）
const COLOR_PALETTE = [
  '#ff7875',
  '#ffa940',
  '#ffc53d',
  '#73d13d',
  '#36cfc9',
  '#40a9ff',
  '#597ef7',
  '#9254de',
  '#f759ab',
  '#8c8c8c',
];

@Injectable()
export class ClassroomRentalsService {
  constructor(
    @InjectRepository(ClassroomRental) private repo: Repository<ClassroomRental>,
    @InjectRepository(Classroom) private classroomRepo: Repository<Classroom>,
    @InjectRepository(Organization) private organizationRepo: Repository<Organization>,
    @InjectRepository(ClassSchedule) private scheduleRepo: Repository<ClassSchedule>,
  ) {}

  get uploadDir(): string {
    const base = process.env.UPLOAD_DIR || './uploads';
    return path.resolve(base, 'contracts');
  }

  ensureUploadDir() {
    if (!fs.existsSync(this.uploadDir)) {
      fs.mkdirSync(this.uploadDir, { recursive: true });
    }
  }

  async findAll(query?: {
    classroomId?: number;
    lesseeOrganizationId?: number;
    month?: string;
    includeEnded?: boolean;
  }) {
    const qb = this.repo
      .createQueryBuilder('r')
      .leftJoinAndSelect('r.classroom', 'classroom')
      .leftJoinAndSelect('r.lesseeOrganization', 'lesseeOrganization')
      .orderBy('r.startDate', 'DESC');
    if (query?.classroomId) qb.andWhere('r.classroomId = :cid', { cid: query.classroomId });
    if (query?.lesseeOrganizationId)
      qb.andWhere('r.lesseeOrganizationId = :oid', { oid: query.lesseeOrganizationId });
    if (query?.month) {
      const [y, m] = query.month.split('-').map(Number);
      const first = `${y}-${String(m).padStart(2, '0')}-01`;
      const lastDay = new Date(y, m, 0).getDate();
      const last = `${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
      qb.andWhere('r.startDate <= :last AND r.endDate >= :first', { first, last });
    }
    if (!query?.includeEnded) {
      qb.andWhere('r.status = :active', { active: ClassroomRentalStatus.ACTIVE });
    }
    const rentals = await qb.getMany();
    return rentals.map((rental) => this.withEffectiveStatus(rental));
  }

  async findOne(id: number) {
    const rental = await this.repo.findOne({
      where: { id },
      relations: ['classroom', 'lessorOrganization', 'lesseeOrganization'],
    });
    if (!rental) throw new NotFoundException('租赁订单不存在');
    return this.withEffectiveStatus(rental);
  }

  async getUnavailableDates(classroomId: number, year: number, month: number, excludeId?: number) {
    const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
    const monthStart = `${year}-${String(month).padStart(2, '0')}-01`;
    const monthEnd = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;

    const [rentals, schedules] = await Promise.all([
      this.repo.find({
        where: {
          ...(excludeId ? { id: Not(excludeId) } : {}),
          classroomId,
          status: ClassroomRentalStatus.ACTIVE,
          startDate: LessThanOrEqual(monthEnd),
          endDate: MoreThanOrEqual(monthStart),
        },
      }),
      this.scheduleRepo.find({
        where: {
          classroomId,
          status: ClassroomRentalStatus.ACTIVE,
          scheduleType: 'INTERNAL',
          startDate: LessThanOrEqual(monthEnd),
          endDate: MoreThanOrEqual(monthStart),
        },
      }),
    ]);

    const unavailableDates = new Set<string>();
    for (const rental of rentals) {
      this.addDateRange(
        unavailableDates,
        rental.startDate > monthStart ? rental.startDate : monthStart,
        rental.endDate < monthEnd ? rental.endDate : monthEnd,
      );
    }
    for (const schedule of schedules) {
      this.addScheduleOccurrences(unavailableDates, schedule, monthStart, monthEnd);
    }

    return { dates: Array.from(unavailableDates).sort() };
  }

  /**
   * 查找与给定区间冲突的租赁订单，同时检测同一教室同一日期段的内部排课
   * 重叠判定：start1 <= end2 AND start2 <= end1
   */
  async findConflicts(classroomId: number, startDate: string, endDate: string, excludeId?: number) {
    const qb = this.repo
      .createQueryBuilder('r')
      .leftJoinAndSelect('r.lesseeOrganization', 'lesseeOrganization')
      .where('r.classroomId = :cid', { cid: classroomId })
      .andWhere('r.status = :active', { active: ClassroomRentalStatus.ACTIVE })
      .andWhere('r.startDate <= :end', { end: endDate })
      .andWhere('r.endDate >= :start', { start: startDate });
    if (excludeId) qb.andWhere('r.id != :excludeId', { excludeId });
    const rentals = await qb.getMany();

    // 检测同一教室同一日期段是否存在内部排课
    const scheduleCandidates = await this.scheduleRepo
      .createQueryBuilder('cs')
      .where('cs.classroomId = :cid', { cid: classroomId })
      .andWhere('cs.status = :status', { status: 'active' })
      .andWhere('cs.scheduleType = :scheduleType', { scheduleType: 'INTERNAL' })
      .andWhere('cs.startDate <= :end', { end: endDate })
      .andWhere('cs.endDate >= :start', { start: startDate })
      .getMany();
    const scheduleConflicts = scheduleCandidates.filter((schedule) =>
      this.hasScheduleOccurrence(schedule, startDate, endDate),
    );

    if (scheduleConflicts.length > 0) {
      throw new ConflictException({
        message: '该教室在此时间段已有排课',
        conflicts: scheduleConflicts.map((s) => ({
          id: s.id,
          startDate: s.startDate,
          endDate: s.endDate,
          organizationName: `[内部排课] ${s.subject}`,
        })),
      });
    }

    return rentals;
  }

  private hasScheduleOccurrence(
    schedule: ClassSchedule,
    startDate: string,
    endDate: string,
  ): boolean {
    const overlapStart = schedule.startDate > startDate ? schedule.startDate : startDate;
    const overlapEnd = schedule.endDate < endDate ? schedule.endDate : endDate;
    if (overlapStart > overlapEnd) return false;

    const startUtc = this.toUtcDate(overlapStart);
    const endUtc = this.toUtcDate(overlapEnd);
    const startWeekDay = startUtc.getUTCDay() || 7;
    const daysUntilOccurrence = (schedule.weekDay - startWeekDay + 7) % 7;
    startUtc.setUTCDate(startUtc.getUTCDate() + daysUntilOccurrence);
    return startUtc <= endUtc;
  }

  private toUtcDate(date: string): Date {
    const [year, month, day] = date.split('-').map(Number);
    return new Date(Date.UTC(year, month - 1, day));
  }

  private addDateRange(dates: Set<string>, startDate: string, endDate: string) {
    const current = this.toUtcDate(startDate);
    const end = this.toUtcDate(endDate);
    while (current <= end) {
      dates.add(current.toISOString().slice(0, 10));
      current.setUTCDate(current.getUTCDate() + 1);
    }
  }

  private addScheduleOccurrences(
    dates: Set<string>,
    schedule: ClassSchedule,
    startDate: string,
    endDate: string,
  ) {
    const overlapStart = schedule.startDate > startDate ? schedule.startDate : startDate;
    const overlapEnd = schedule.endDate < endDate ? schedule.endDate : endDate;
    if (overlapStart > overlapEnd) return;

    const current = this.toUtcDate(overlapStart);
    const end = this.toUtcDate(overlapEnd);
    const startWeekDay = current.getUTCDay() || 7;
    current.setUTCDate(current.getUTCDate() + ((schedule.weekDay - startWeekDay + 7) % 7));
    while (current <= end) {
      dates.add(current.toISOString().slice(0, 10));
      current.setUTCDate(current.getUTCDate() + 7);
    }
  }

  async create(dto: CreateRentalDto, userId?: number) {
    if (dto.startDate > dto.endDate) throw new BadRequestException('起始日期不能晚于结束日期');
    const classroom = await this.classroomRepo.findOne({ where: { id: dto.classroomId } });
    if (!classroom) throw new NotFoundException('教室不存在');
    if (classroom.status !== ClassroomStatus.AVAILABLE) {
      throw new BadRequestException('仅可用教室可以创建租赁');
    }
    const lessorOrganization = dto.lessorOrganizationId
      ? await this.organizationRepo.findOne({
          where: { id: dto.lessorOrganizationId, status: 'active' },
        })
      : await this.organizationRepo.findOne({ where: { isHost: true, status: 'active' } });
    if (!lessorOrganization) throw new NotFoundException('出租机构不存在或未启用');
    const lesseeOrganization = await this.organizationRepo.findOne({
      where: { id: dto.lesseeOrganizationId, status: 'active' },
    });
    if (!lesseeOrganization) throw new NotFoundException('承租机构不存在或未启用');
    if (lessorOrganization.id === lesseeOrganization.id) {
      throw new BadRequestException('出租机构和承租机构不能相同');
    }

    const conflicts = await this.findConflicts(dto.classroomId, dto.startDate, dto.endDate);
    if (conflicts.length > 0) {
      throw new ConflictException({
        message: '该教室在此时间段已有租赁',
        conflicts: conflicts.map((c) => ({
          id: c.id,
          startDate: c.startDate,
          endDate: c.endDate,
          organizationName: c.lesseeOrganization?.name,
        })),
      });
    }
    const rental = this.repo.create({
      ...dto,
      lessorOrganizationId: lessorOrganization.id,
      lesseeOrganizationId: lesseeOrganization.id,
      createdBy: userId,
      status: ClassroomRentalStatus.ACTIVE,
    });
    const saved = await this.repo.save(rental);
    await this.syncScheduleFromRental(saved, lesseeOrganization.name);
    return saved;
  }

  async update(id: number, dto: UpdateRentalDto) {
    const rental = await this.findOne(id);
    if (rental.effectiveStatus !== ClassroomRentalStatus.ACTIVE) {
      throw new BadRequestException('已结束或已取消的租赁不能编辑');
    }
    // 若修改了教室/日期，重新冲突检查
    const newClassroomId = dto.classroomId ?? rental.classroomId;
    const newStart = dto.startDate ?? rental.startDate;
    const newEnd = dto.endDate ?? rental.endDate;
    if (newStart > newEnd) throw new BadRequestException('起始日期不能晚于结束日期');
    if (dto.classroomId && dto.classroomId !== rental.classroomId) {
      const classroom = await this.classroomRepo.findOne({ where: { id: dto.classroomId } });
      if (!classroom) throw new NotFoundException('教室不存在');
      if (classroom.status !== ClassroomStatus.AVAILABLE) {
        throw new BadRequestException('仅可用教室可以承接租赁');
      }
    }
    if (dto.classroomId || dto.startDate || dto.endDate) {
      const conflicts = await this.findConflicts(newClassroomId, newStart, newEnd, id);
      if (conflicts.length > 0) {
        throw new ConflictException({
          message: '修改后时间段与已有租赁冲突',
          conflicts: conflicts.map((c) => ({
            id: c.id,
            startDate: c.startDate,
            endDate: c.endDate,
            organizationName: c.lesseeOrganization?.name,
          })),
        });
      }
    }
    const newLessorId = dto.lessorOrganizationId ?? rental.lessorOrganizationId;
    const newLesseeId = dto.lesseeOrganizationId ?? rental.lesseeOrganizationId;
    if (newLessorId === newLesseeId) {
      throw new BadRequestException('出租机构和承租机构不能相同');
    }
    if (dto.lessorOrganizationId) {
      const lessor = await this.organizationRepo.findOne({
        where: { id: dto.lessorOrganizationId, status: 'active' },
      });
      if (!lessor) throw new NotFoundException('出租机构不存在或未启用');
    }
    if (dto.lesseeOrganizationId) {
      const lessee = await this.organizationRepo.findOne({
        where: { id: dto.lesseeOrganizationId, status: 'active' },
      });
      if (!lessee) throw new NotFoundException('承租机构不存在或未启用');
    }
    await this.repo.update(id, dto);
    const updated = await this.findOne(id);
    await this.syncScheduleFromRental(updated);
    return updated;
  }

  async cancel(id: number) {
    const rental = await this.findOne(id);
    if (rental.effectiveStatus !== ClassroomRentalStatus.ACTIVE) {
      throw new BadRequestException('仅有效租赁可以取消');
    }
    await this.repo.update(id, { status: ClassroomRentalStatus.CANCELLED });
    await this.scheduleRepo.update({ rentalId: id, scheduleType: 'RENTAL' }, { status: 'inactive' });
    return this.findOne(id);
  }

  async end(id: number) {
    const rental = await this.findOne(id);
    if (rental.effectiveStatus !== ClassroomRentalStatus.ACTIVE) {
      throw new BadRequestException('仅有效租赁可以结束');
    }
    const today = new Intl.DateTimeFormat('en-CA', {
      timeZone: 'Asia/Shanghai',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
    }).format(new Date());
    if (rental.startDate > today) throw new BadRequestException('租赁尚未开始，不能结束');
    await this.repo.update(id, {
      status: ClassroomRentalStatus.ENDED,
      endDate: rental.endDate > today ? today : rental.endDate,
    });
    const ended = await this.findOne(id);
    await this.syncScheduleFromRental(ended);
    return ended;
  }

  async remove(id: number) {
    const rental = await this.findOne(id);
    if (rental.status === ClassroomRentalStatus.CANCELLED) {
      return { message: '租赁订单已归档' };
    }
    await this.repo.update(id, { status: ClassroomRentalStatus.CANCELLED });
    await this.scheduleRepo.update({ rentalId: id, scheduleType: 'RENTAL' }, { status: 'inactive' });
    return { message: '租赁订单已归档（合同文件已保留）' };
  }

  private withEffectiveStatus(rental: ClassroomRental) {
    const today = new Intl.DateTimeFormat('en-CA', {
      timeZone: 'Asia/Shanghai',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
    }).format(new Date());
    const effectiveStatus =
      rental.status === ClassroomRentalStatus.ACTIVE && rental.endDate < today
        ? ClassroomRentalStatus.ENDED
        : rental.status;
    return Object.assign(rental, { effectiveStatus });
  }

  /**
   * 同步租赁订单到 class_schedules（schedule_type = 'RENTAL'）
   */
  private async syncScheduleFromRental(rental: ClassroomRental, organizationName?: string) {
    const name = organizationName || rental.lesseeOrganization?.name || '承租机构';
    const weekDay = this.dateToWeekDay(rental.startDate);
    let schedule = await this.scheduleRepo.findOne({
      where: { rentalId: rental.id, scheduleType: 'RENTAL' },
    });
    const data = {
      classroomId: rental.classroomId,
      classId: null,
      weekDay,
      startTime: '00:00',
      endTime: '23:59',
      startDate: rental.startDate,
      endDate: rental.endDate,
      subject: `${name} 租赁`,
      teacherId: null,
      scheduleType: 'RENTAL',
      rentalId: rental.id,
      status: rental.status === ClassroomRentalStatus.CANCELLED ? 'cancelled' : 'active',
      notes: rental.notes,
    };
    if (schedule) {
      await this.scheduleRepo.update(schedule.id, data);
    } else {
      schedule = this.scheduleRepo.create(data);
      await this.scheduleRepo.save(schedule);
    }
  }

  private dateToWeekDay(date: string): number {
    const d = new Date(date);
    const day = d.getDay();
    return day === 0 ? 7 : day;
  }

  async attachContract(id: number, file: Express.Multer.File) {
    const rental = await this.findOne(id);
    this.ensureUploadDir();
    // 安全校验：MIME + 扩展名
    if (file.mimetype !== 'application/pdf') {
      throw new BadRequestException('仅支持 PDF 文件');
    }
    const ext = path.extname(file.originalname).toLowerCase();
    if (ext !== '.pdf') throw new BadRequestException('文件扩展名必须为 .pdf');
    // UUID 文件名
    const uuid =
      (globalThis as any).crypto?.randomUUID?.() ||
      require('crypto').randomBytes(16).toString('hex');
    const filename = `${uuid}.pdf`;
    const fullPath = path.join(this.uploadDir, filename);
    // 路径遍历防护
    if (!fullPath.startsWith(this.uploadDir)) throw new BadRequestException('路径非法');
    // 替换旧文件
    if (rental.contractPath) {
      const oldPath = path.join(this.uploadDir, rental.contractPath);
      if (fs.existsSync(oldPath)) {
        try {
          fs.unlinkSync(oldPath);
        } catch {
          /* ignore */
        }
      }
    }
    fs.writeFileSync(fullPath, file.buffer);
    await this.repo.update(id, {
      contractPath: filename,
      contractOriginalName: file.originalname,
    });
    return this.findOne(id);
  }

  async removeContract(id: number) {
    const rental = await this.findOne(id);
    if (!rental.contractPath) throw new BadRequestException('该租赁未上传合同');
    const fullPath = path.join(this.uploadDir, rental.contractPath);
    if (fs.existsSync(fullPath)) {
      try {
        fs.unlinkSync(fullPath);
      } catch {
        /* ignore */
      }
    }
    await this.repo.update(id, { contractPath: null as any, contractOriginalName: null as any });
    return { message: '合同已移除' };
  }

  /**
   * 获取合同文件的绝对路径（供控制器流式返回），严格校验路径安全
   */
  async getContractPath(id: number): Promise<{ fullPath: string; originalName: string }> {
    const rental = await this.findOne(id);
    if (!rental.contractPath) throw new NotFoundException('该租赁未上传合同');
    const fullPath = path.join(this.uploadDir, rental.contractPath);
    if (!fullPath.startsWith(this.uploadDir)) throw new BadRequestException('路径非法');
    if (!fs.existsSync(fullPath)) throw new NotFoundException('合同文件丢失');
    return { fullPath, originalName: rental.contractOriginalName || 'contract.pdf' };
  }

  /**
   * 获取月度排期矩阵
   */
  async getSchedule(year: number, month: number) {
    const lastDay = new Date(year, month, 0).getDate();
    const first = `${year}-${String(month).padStart(2, '0')}-01`;
    const last = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;

    const classrooms = await this.classroomRepo.find({
      where: { status: Not(ClassroomStatus.ARCHIVED) },
      order: { building: 'ASC', name: 'ASC' },
    });
    const rentals = await this.repo
      .createQueryBuilder('r')
      .leftJoinAndSelect('r.lesseeOrganization', 'lesseeOrganization')
      .leftJoinAndSelect('r.classroom', 'classroom')
      .where('r.status IN (:...statuses)', {
        statuses: [ClassroomRentalStatus.ACTIVE, ClassroomRentalStatus.ENDED],
      })
      .andWhere('r.startDate <= :last AND r.endDate >= :first', { first, last })
      .getMany();

    const organizationMap = new Map<number, any>();
    const matrix: Record<number, Record<number, any>> = {};
    const summary: Record<
      number,
      { totalDays: number; rentedDays: number; idleDays: number; occupancyRate: number }
    > = {};

    for (const cls of classrooms) {
      matrix[cls.id] = {};
      summary[cls.id] = { totalDays: lastDay, rentedDays: 0, idleDays: lastDay, occupancyRate: 0 };
    }

    for (const rental of rentals) {
      const start = new Date(rental.startDate);
      const end = new Date(rental.endDate);
      const monthStart = new Date(first);
      const monthEnd = new Date(last);
      const effStart = start < monthStart ? monthStart : start;
      const effEnd = end > monthEnd ? monthEnd : end;
      if (rental.lesseeOrganization && !organizationMap.has(rental.lesseeOrganization.id)) {
        organizationMap.set(rental.lesseeOrganization.id, {
          id: rental.lesseeOrganization.id,
          name: rental.lesseeOrganization.name,
          color:
            rental.lesseeOrganization.color ||
            COLOR_PALETTE[rental.lesseeOrganization.id % COLOR_PALETTE.length],
        });
      }
      for (let d = new Date(effStart); d <= effEnd; d.setDate(d.getDate() + 1)) {
        const day = d.getDate();
        if (!matrix[rental.classroomId]) continue;
        matrix[rental.classroomId][day] = {
          scheduleType: 'RENTAL',
          rentalId: rental.id,
          organizationId: rental.lesseeOrganizationId,
          organizationName: rental.lesseeOrganization?.name || '未知',
          color:
            rental.lesseeOrganization?.color ||
            COLOR_PALETTE[(rental.lesseeOrganizationId || 0) % COLOR_PALETTE.length],
          hasContract: !!rental.contractPath,
        };
      }
    }

    // ── Overlay internal class schedules ──
    const schedules = await this.scheduleRepo
      .createQueryBuilder('s')
      .leftJoinAndSelect('s.class', 'class')
      .leftJoinAndSelect('s.teacher', 'teacher')
      .where('s.status = :active', { active: 'active' })
      .andWhere('s.scheduleType = :type', { type: 'INTERNAL' })
      .andWhere('s.startDate <= :last AND s.endDate >= :first', { first, last })
      .getMany();

    for (const sched of schedules) {
      if (!sched.classroomId) continue;
      const schedStart = new Date(
        Math.max(new Date(sched.startDate).getTime(), new Date(first).getTime()),
      );
      const schedEnd = new Date(
        Math.min(new Date(sched.endDate).getTime(), new Date(last).getTime()),
      );
      for (let d = new Date(schedStart); d <= schedEnd; d.setDate(d.getDate() + 1)) {
        const dow = d.getDay() === 0 ? 7 : d.getDay();
        if (dow !== sched.weekDay) continue;
        const day = d.getDate();
        if (!matrix[sched.classroomId]) continue;
        matrix[sched.classroomId][day] = {
          scheduleType: 'INTERNAL',
          scheduleId: sched.id,
          className: (sched.class as any)?.name || '',
          subject: sched.subject,
          teacherName: (sched.teacher as any)?.name || '',
          startTime: sched.startTime,
          endTime: sched.endTime,
          color: '#52c41a',
        };
      }
    }
    // 统计
    for (const cls of classrooms) {
      const rented = Object.keys(matrix[cls.id]).length;
      summary[cls.id].rentedDays = rented;
      summary[cls.id].idleDays = lastDay - rented;
      summary[cls.id].occupancyRate = lastDay > 0 ? Math.round((rented / lastDay) * 100) / 100 : 0;
    }

    return {
      year,
      month,
      days: lastDay,
      classrooms: classrooms.map((c) => ({
        id: c.id,
        name: c.name,
        building: c.building,
        floor: c.floor,
        roomType: c.roomType,
        capacity: c.capacity,
      })),
      organizations: Array.from(organizationMap.values()),
      matrix,
      summary,
    };
  }
}
