import { HttpStatus, Injectable } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import {
  appointmentWithinWorkingHours,
  blocksForDay,
  localDayOfWeekMondayZero,
  validateWorkingHoursBlocks,
  type WorkingHoursBlockInput,
} from '../../common/working-hours';
import { isValidIanaTimeZone, zonedWeekdayAndMinutes } from '../../common/zoned-civil-time';
import { UpsertWorkingHoursDto } from './dto/upsert-working-hours.dto';
import {
  hasEffectivePermission,
} from '../../common/membership-permissions';
import { AppException, ErrorCode } from '../../common/errors';

@Injectable()
export class StaffWorkingHoursService {
  constructor(private readonly prisma: PrismaService) {}

  async getWorkingHours(actorUserId: string, organizationId: string, membershipId: string) {
    await this.assertCanViewStaff(actorUserId, organizationId);

    const membership = await this.findStaffMembership(membershipId, organizationId);
    const schedule = await this.prisma.staffWorkingHoursSchedule.findUnique({
      where: { membershipId: membership.id },
      include: {
        blocks: { orderBy: [{ dayOfWeek: 'asc' }, { sortOrder: 'asc' }, { startMinute: 'asc' }] },
      },
    });

    if (!schedule) {
      return {
        success: true,
        data: {
          autoRepeatWeekly: true,
          blocks: [],
          hasWorkingHours: false,
        },
      };
    }

    return {
      success: true,
      data: {
        autoRepeatWeekly: schedule.autoRepeatWeekly,
        blocks: schedule.blocks.map((b) => ({
          dayOfWeek: b.dayOfWeek,
          startMinute: b.startMinute,
          endMinute: b.endMinute,
          sortOrder: b.sortOrder,
        })),
        hasWorkingHours: schedule.blocks.length > 0,
      },
    };
  }

  async upsertWorkingHours(
    actorUserId: string,
    organizationId: string,
    membershipId: string,
    dto: UpsertWorkingHoursDto,
  ) {
    await this.assertCanEditStaff(actorUserId, organizationId);

    const membership = await this.findStaffMembership(membershipId, organizationId);
    return this.persistWorkingHours(membership, organizationId, dto);
  }

  async getMyWorkingHours(userId: string, organizationId: string) {
    const membership = await this.findOwnerMembership(userId, organizationId);
    await this.assertOwnerCanManageWorkingHours(membership);

    const schedule = await this.prisma.staffWorkingHoursSchedule.findUnique({
      where: { membershipId: membership.id },
      include: {
        blocks: { orderBy: [{ dayOfWeek: 'asc' }, { sortOrder: 'asc' }, { startMinute: 'asc' }] },
      },
    });

    if (!schedule) {
      return {
        success: true,
        data: {
          autoRepeatWeekly: true,
          blocks: [],
          hasWorkingHours: false,
        },
      };
    }

    return {
      success: true,
      data: {
        autoRepeatWeekly: schedule.autoRepeatWeekly,
        blocks: schedule.blocks.map((b) => ({
          dayOfWeek: b.dayOfWeek,
          startMinute: b.startMinute,
          endMinute: b.endMinute,
          sortOrder: b.sortOrder,
        })),
        hasWorkingHours: schedule.blocks.length > 0,
      },
    };
  }

  async upsertMyWorkingHours(
    userId: string,
    organizationId: string,
    dto: UpsertWorkingHoursDto,
  ) {
    const membership = await this.findOwnerMembership(userId, organizationId);
    await this.assertOwnerCanManageWorkingHours(membership);
    return this.persistWorkingHours(membership, organizationId, dto);
  }

  private async persistWorkingHours(
    membership: { id: string; userId: string },
    organizationId: string,
    dto: UpsertWorkingHoursDto,
  ) {
    const validationError = validateWorkingHoursBlocks(dto.blocks);
    if (validationError) {
      throw new AppException(ErrorCode.WORKING_HOURS_INVALID, HttpStatus.BAD_REQUEST);
    }

    const timeZone = this.requireTimeZone(dto.timeZone);
    const normalizedBlocks = this.normalizeBlocks(dto.blocks);
    await this.assertNoConflictingAppointments(
      organizationId,
      membership.userId,
      normalizedBlocks,
      timeZone,
    );

    await this.prisma.$transaction(async (tx) => {
      const schedule = await tx.staffWorkingHoursSchedule.upsert({
        where: { membershipId: membership.id },
        create: {
          membershipId: membership.id,
          autoRepeatWeekly: dto.autoRepeatWeekly,
        },
        update: {
          autoRepeatWeekly: dto.autoRepeatWeekly,
        },
      });

      await tx.staffWorkingHoursBlock.deleteMany({ where: { scheduleId: schedule.id } });

      if (normalizedBlocks.length > 0) {
        await tx.staffWorkingHoursBlock.createMany({
          data: normalizedBlocks.map((block, index) => ({
            scheduleId: schedule.id,
            dayOfWeek: block.dayOfWeek,
            startMinute: block.startMinute,
            endMinute: block.endMinute,
            sortOrder: block.sortOrder ?? index,
          })),
        });
      }
    });

    return {
      success: true,
      message: 'Working hours saved',
    };
  }

  async loadScheduleBlocksByMembershipIds(membershipIds: string[]) {
    if (membershipIds.length === 0) {
      return new Map<string, WorkingHoursBlockInput[]>();
    }

    const schedules = await this.prisma.staffWorkingHoursSchedule.findMany({
      where: { membershipId: { in: membershipIds } },
      include: {
        blocks: { orderBy: [{ dayOfWeek: 'asc' }, { sortOrder: 'asc' }, { startMinute: 'asc' }] },
      },
    });

    const map = new Map<string, WorkingHoursBlockInput[]>();
    for (const schedule of schedules) {
      map.set(
        schedule.membershipId,
        schedule.blocks.map((b) => ({
          dayOfWeek: b.dayOfWeek,
          startMinute: b.startMinute,
          endMinute: b.endMinute,
          sortOrder: b.sortOrder,
        })),
      );
    }
    return map;
  }

  dayBlocksFromMembershipBlocks(blocks: WorkingHoursBlockInput[], dayOfWeekMondayZero: number) {
    return blocksForDay(blocks, dayOfWeekMondayZero);
  }

  private normalizeBlocks(blocks: UpsertWorkingHoursDto['blocks']): WorkingHoursBlockInput[] {
    return blocks.map((block, index) => ({
      dayOfWeek: block.dayOfWeek,
      startMinute: block.startMinute,
      endMinute: block.endMinute,
      sortOrder: block.sortOrder ?? index,
    }));
  }

  private requireTimeZone(timeZone: string | undefined): string {
    if (!timeZone || !isValidIanaTimeZone(timeZone)) {
      throw new AppException(ErrorCode.VALIDATION_TIMEZONE_INVALID, HttpStatus.BAD_REQUEST);
    }
    return timeZone;
  }

  private async assertNoConflictingAppointments(
    organizationId: string,
    providerUserId: string,
    blocks: WorkingHoursBlockInput[],
    timeZone: string,
  ) {
    const now = new Date();
    const appointments = await this.prisma.appointment.findMany({
      where: {
        organizationId,
        providerUserId,
        endAt: { gt: now },
      },
      select: { startAt: true, endAt: true },
      orderBy: { startAt: 'asc' },
    });

    const conflicts = appointments.filter((appointment) => {
      const startAt = new Date(appointment.startAt);
      const endAt = new Date(appointment.endAt);
      const dayOfWeek = localDayOfWeekMondayZero(
        zonedWeekdayAndMinutes(startAt, timeZone).jsWeekday,
      );
      const dayBlocks = blocksForDay(blocks, dayOfWeek);
      if (dayBlocks.length === 0) {
        return true;
      }
      return !appointmentWithinWorkingHours(startAt, endAt, dayBlocks, timeZone);
    });

    if (conflicts.length === 0) {
      return;
    }

    throw new AppException(
      ErrorCode.WORKING_HOURS_CONFLICTS_WITH_APPOINTMENTS,
      HttpStatus.CONFLICT,
    );
  }

  private async findStaffMembership(membershipId: string, organizationId: string) {
    const membership = await this.prisma.membership.findFirst({
      where: { id: membershipId, organizationId },
      select: { id: true, isOwner: true, userId: true },
    });
    if (!membership) {
      throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
    if (membership.isOwner) {
      throw new AppException(ErrorCode.WORKING_HOURS_OWNER_NOT_ALLOWED, HttpStatus.BAD_REQUEST);
    }
    return membership;
  }

  private async findOwnerMembership(userId: string, organizationId: string) {
    const membership = await this.prisma.membership.findFirst({
      where: { userId, organizationId, isOwner: true },
      include: {
        permissions: { include: { permission: true } },
        organization: { include: { type: true, plan: true } },
      },
    });
    if (!membership) {
      throw new AppException(ErrorCode.PERMISSION_OWNER_ONLY, HttpStatus.FORBIDDEN);
    }
    return membership;
  }

  private async assertOwnerCanManageWorkingHours(membership: {
    isOwner: boolean;
    permissions: { permission: { name: string } }[];
    organization: { type: { name: string }; plan?: { name: string } | null; planId?: string | null };
  }) {
    if (!hasEffectivePermission(membership, 'TAB_TREATMENT_EDIT')) {
      throw new AppException(ErrorCode.PERMISSION_ENABLE_PARTICIPATION_FIRST, HttpStatus.FORBIDDEN);
    }
  }

  private async assertCanViewStaff(userId: string, organizationId: string) {
    const actor = await this.getActorMembership(userId, organizationId);
    if (!actor || !this.canViewStaff(actor)) {
      throw new AppException(ErrorCode.PERMISSION_ACCESS_STAFF, HttpStatus.FORBIDDEN);
    }
  }

  private async assertCanEditStaff(userId: string, organizationId: string) {
    const actor = await this.getActorMembership(userId, organizationId);
    if (!actor || !this.canEditStaff(actor)) {
      throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
    }
  }

  private async getActorMembership(userId: string, organizationId: string) {
    return this.prisma.membership.findFirst({
      where: { userId, organizationId },
      include: {
        permissions: { include: { permission: true } },
        organization: { select: { planId: true } },
      },
    });
  }

  private canViewStaff(m: {
    isOwner: boolean;
    permissions: { permission: { name: string } }[];
  }): boolean {
    if (m.isOwner) return true;
    return m.permissions.some(
      (p) => p.permission.name === 'TAB_STAFF_READ' || p.permission.name === 'TAB_STAFF_EDIT',
    );
  }

  private canEditStaff(m: {
    isOwner: boolean;
    organization?: { planId: string | null };
    permissions: { permission: { name: string } }[];
  }): boolean {
    if (m.isOwner) return Boolean(m.organization?.planId);
    return m.permissions.some((p) => p.permission.name === 'TAB_STAFF_EDIT');
  }
}
