import { BadRequestError, BaseAdapter, ErrorCodes, NotFoundError } from '@humand-packages/common';
import { Service } from 'typedi';
import { PortsDI } from '../../../../../config/portsDI';
import { LimitOffsetPagination } from '../../../../../types/listOptions';
import ShiftRotationRepositoryPort, { ShiftRotationsListFilters } from '../../../infrastructure/ports/shiftManagement/shiftRotationRepositoryPort';
import { CreateOrUpdateShiftRotationVC } from '../../../presentation/validationClasses/shiftManagement/createOrUpdateShiftRotationVC';
import { ShiftRotationDayShiftVC } from '../../../presentation/validationClasses/shiftManagement/shiftRotationDayShiftVC';
import { ShiftRotationDayVC } from '../../../presentation/validationClasses/shiftManagement/shiftRotationDayVC';
import { ShiftRotationDayType } from '../../enum/shiftManagement/shiftRotationDayType';
import { mapRotationDayShiftVCToShiftInput } from '../../mappers/shiftManagement/mapRotationDayShiftVCToShiftInput';
import { Shift } from '../../models/shiftManagement/shift';
import {
    ShiftRotation,
    ShiftRotationAggregates,
    ShiftRotationDayForCreation,
    ShiftRotationDayShiftForCreation,
    ShiftRotationForCreation,
    ShiftRotationListItem,
} from '../../models/shiftManagement/shiftRotation';
import { RotationDayPlan } from '../../types/shiftManagement/rotationDayPlan';
import { ShiftRotationWithShifts } from '../../types/shiftManagement/shiftRotationWithShifts';
import { intervalsOverlap, timeSlotHours, toMinutes } from '../../utils/timeSlotOverlap';
import { ShiftService } from './shiftService';

const MINUTES_PER_DAY = 1440;

@Service()
export class ShiftRotationService extends BaseAdapter {
    constructor(
        @PortsDI(ShiftRotationRepositoryPort)
        private readonly repository: ShiftRotationRepositoryPort,
        private readonly shiftService: ShiftService,
    ) {
        super();
    }

    public create = async (instanceId: number, params: CreateOrUpdateShiftRotationVC): Promise<ShiftRotationWithShifts> => {
        this.assertNoTimeOverlap(params);
        return this.repository.getTransaction(async () => {
            const daysPlan = await this.resolveDaysForCreate(instanceId, params.days);
            const forCreation = this.buildForCreation(instanceId, params.name, params.description, daysPlan);
            const rotation = await this.repository.create(instanceId, forCreation);
            const shiftsByCode = await this.loadShiftsByCodes(instanceId, this.collectShiftCodes([rotation]));
            return { rotation, shiftsByCode };
        });
    };

    /**
     * For each WORK-day shift entry in the body:
     *
     * - `shiftCode` PRESENT, matches one of this rotation's current codes:
     *   - Definition unchanged → no-op.
     *   - Definition changed → insert a new `Shifts` row with the new definition bumping the version.
     *
     * - `shiftCode` ABSENT: create a new `Shifts` row with the new definition.
     *
     * After every body entry is resolved to a concrete code, any code that *was* in the
     * rotation's previous graph but is *not* in the resolved one is soft-deleted
     * Day positions are derived from the array order. Moving a shift to a different
     * day position is a re-wiring of `ShiftRotationDayShifts`
     */
    public update = async (instanceId: number, id: string, params: CreateOrUpdateShiftRotationVC): Promise<ShiftRotationWithShifts> => {
        this.assertNoTimeOverlap(params);
        return this.repository.getTransaction(async () => {
            const existing = await this.repository.getById(instanceId, id);
            if (!existing) {
                throw new NotFoundError('Shift rotation not found', ErrorCodes.SHIFT_ROTATION_NOT_FOUND);
            }

            const existingCodes = this.collectShiftCodes([existing]);
            this.assertCodesBelongToRotation(params, existingCodes);

            const existingShiftsByCode = await this.loadShiftsByCodes(instanceId, existingCodes);
            const daysPlan = await this.resolveDaysForUpdate(instanceId, params.days, existingShiftsByCode);

            const referencedCodes = new Set<string>();
            for (const day of daysPlan) {
                for (const shift of day.shifts) referencedCodes.add(shift.shiftCode);
            }
            const removedCodes = existingCodes.filter((c) => !referencedCodes.has(c));
            if (removedCodes.length > 0) {
                await this.shiftService.deleteByCodes(instanceId, removedCodes);
            }

            const forCreation = this.buildForCreation(instanceId, params.name, params.description, daysPlan);
            const rotation = await this.repository.replaceDays(instanceId, existing, forCreation);
            const shiftsByCode = await this.loadShiftsByCodes(instanceId, this.collectShiftCodes([rotation]));
            return { rotation, shiftsByCode };
        });
    };

    /**
     * Loads the rotation graph plus a `shiftsByCode` map populated by following the
     * junction codes through `Shifts`.
     */
    public getById = async (instanceId: number, id: string): Promise<ShiftRotationWithShifts> => {
        const found = await this.repository.getById(instanceId, id);
        if (!found) {
            throw new NotFoundError('Shift rotation not found', ErrorCodes.SHIFT_ROTATION_NOT_FOUND);
        }
        const shiftsByCode = await this.loadShiftsByCodes(instanceId, this.collectShiftCodes([found]));
        return { rotation: found, shiftsByCode };
    };

    /**
     * Paginated list with per-rotation aggregates (cycle length, work/rest day counts,
     * total hours). Loads the referenced `Shifts` for every rotation in the page in one
     * query and computes aggregates from their time slots in memory.
     */
    public list = async (
        instanceId: number,
        filters: ShiftRotationsListFilters,
        pagination: LimitOffsetPagination,
    ): Promise<{ count: number; rows: ShiftRotationListItem[] }> => {
        const { count, rows: rotations } = await this.repository.list(instanceId, filters, pagination);
        if (rotations.length === 0) return { count, rows: [] };

        const shiftsByCode = await this.loadShiftsByCodes(instanceId, this.collectShiftCodes(rotations));

        const items = rotations.map(
            (rotation) =>
                new ShiftRotationListItem({
                    createdAt: rotation.createdAt,
                    description: rotation.description,
                    id: rotation.id,
                    name: rotation.name,
                    updatedAt: rotation.updatedAt,
                    ...this.computeAggregates(rotation, shiftsByCode),
                }),
        );
        return { count, rows: items };
    };

    /**
     * Soft-deletes the rotation and cascade-soft-deletes its rotation-owned `Shifts`
     */
    public delete = async (instanceId: number, id: string): Promise<void> => {
        await this.repository.getTransaction(async () => {
            const existing = await this.repository.getById(instanceId, id);
            if (!existing) {
                throw new NotFoundError('Shift rotation not found', ErrorCodes.SHIFT_ROTATION_NOT_FOUND);
            }
            const codes = this.collectShiftCodes([existing]);
            await this.repository.softDelete(instanceId, id);
            if (codes.length > 0) {
                await this.shiftService.deleteByCodes(instanceId, codes);
            }
        });
    };

    /**
     * Builds the `RotationDayPlan[]` for the create flow: for each WORK-day shift, writes
     * a brand-new `Shifts` row via `shiftService.createShift(saveAsTemplate=false)` and
     * records its generated `shiftCode` in the plan. REST days carry no shifts.
     *
     * Output preserves input day order; the caller maps array index → `dayPosition`.
     */
    private resolveDaysForCreate = async (instanceId: number, days: ShiftRotationDayVC[]): Promise<RotationDayPlan[]> => {
        const plan: RotationDayPlan[] = [];
        for (const day of days) {
            if (day.type !== ShiftRotationDayType.WORK) {
                plan.push({ shifts: [], type: day.type });
                continue;
            }
            const shifts: { shiftCode: string }[] = [];
            for (const s of day.shifts) {
                const created = await this.shiftService.createShift(instanceId, false, mapRotationDayShiftVCToShiftInput(s));
                shifts.push({ shiftCode: created.code });
            }
            plan.push({ shifts, type: day.type });
        }
        return plan;
    };

    /**
     * Builds the `RotationDayPlan[]` for the update flow against the rotation's current
     * Shifts. For each WORK-day shift entry the decision is:
     *
     * - `shiftCode` and unchanged → no-op.
     * - `shiftCode` and content differs → insert a new `Shifts` row with the new definition bumping the version.
     * - `shiftCode` but not owned by this rotation → throws `BadRequestError(SHIFT_ROTATION_INVALID_SHIFT_CODE)`.
     * - `shiftCode` absent → create a new `Shifts` row with the new definition.
     */
    private resolveDaysForUpdate = async (
        instanceId: number,
        days: ShiftRotationDayVC[],
        existingByCode: Map<string, Shift>,
    ): Promise<RotationDayPlan[]> => {
        const plan: RotationDayPlan[] = [];
        for (const day of days) {
            if (day.type !== ShiftRotationDayType.WORK) {
                plan.push({ shifts: [], type: day.type });
                continue;
            }
            const shifts: { shiftCode: string }[] = [];
            for (const s of day.shifts) {
                if (s.shiftCode) {
                    const existing = existingByCode.get(s.shiftCode);
                    if (!existing) {
                        throw new BadRequestError(
                            `Shift code ${s.shiftCode} does not belong to this rotation`,
                            ErrorCodes.SHIFT_ROTATION_INVALID_SHIFT_CODE,
                            { invalidShiftCode: s.shiftCode },
                        );
                    }
                    if (this.shiftDefinitionChanged(existing, s)) {
                        await this.shiftService.bumpShiftVersion(instanceId, existing, mapRotationDayShiftVCToShiftInput(s));
                    }
                    shifts.push({ shiftCode: s.shiftCode });
                } else {
                    const created = await this.shiftService.createShift(instanceId, false, mapRotationDayShiftVCToShiftInput(s));
                    shifts.push({ shiftCode: created.code });
                }
            }
            plan.push({ shifts, type: day.type });
        }
        return plan;
    };

    private buildForCreation = (
        instanceId: number,
        name: string,
        description: string | undefined,
        days: RotationDayPlan[],
    ): ShiftRotationForCreation => {
        return new ShiftRotationForCreation({
            days: days.map(
                (day, index) =>
                    new ShiftRotationDayForCreation({
                        dayPosition: index + 1,
                        instanceId,
                        shifts: day.shifts.map((s) => new ShiftRotationDayShiftForCreation({ instanceId, shiftCode: s.shiftCode })),
                        type: day.type,
                    }),
            ),
            description: description ?? null,
            instanceId,
            name,
        });
    };

    private assertCodesBelongToRotation = (params: CreateOrUpdateShiftRotationVC, existingCodes: string[]): void => {
        const existingSet = new Set(existingCodes);
        for (const day of params.days) {
            if (day.type !== ShiftRotationDayType.WORK) continue;
            for (const s of day.shifts) {
                if (s.shiftCode && !existingSet.has(s.shiftCode)) {
                    throw new BadRequestError(
                        `Shift code ${s.shiftCode} does not belong to this rotation`,
                        ErrorCodes.SHIFT_ROTATION_INVALID_SHIFT_CODE,
                        { invalidShiftCode: s.shiftCode },
                    );
                }
            }
        }
    };

    /**
     * Compares the persisted Shift against the incoming VC payload. Returns true iff any
     * user-visible field differs: `name`, `color`, `description`, or the ordered list of
     * time slots (position-sensitive — reordering counts as a change).
     *
     * Used by `resolveDaysForUpdate` to gate `bumpShiftVersion`: identical content → no-op,
     * any difference → version bump.
     */
    private shiftDefinitionChanged = (existing: Shift, incoming: ShiftRotationDayShiftVC): boolean => {
        if (existing.name !== incoming.name) return true;
        if ((existing.color ?? null) !== (incoming.color ?? null)) return true;
        if ((existing.description ?? null) !== (incoming.description ?? null)) return true;
        if (existing.timeSlots.length !== incoming.timeSlots.length) return true;
        const existingSlots = [...existing.timeSlots];
        for (let i = 0; i < existingSlots.length; i++) {
            const a = existingSlots[i];
            const [start, end] = incoming.timeSlots[i];
            if (a.startTime !== start || a.endTime !== end) return true;
        }
        return false;
    };

    private collectShiftCodes = (rotations: ShiftRotation[]): string[] => {
        const codes = new Set<string>();
        for (const rotation of rotations) {
            for (const day of rotation.days) {
                for (const dayShift of day.shifts) codes.add(dayShift.shiftCode);
            }
        }
        return Array.from(codes);
    };

    private loadShiftsByCodes = async (instanceId: number, codes: string[]): Promise<Map<string, Shift>> => {
        if (codes.length === 0) return new Map();
        const shifts = await this.shiftService.getShiftsByCodes(instanceId, codes);
        return new Map(shifts.map((s) => [s.code, s]));
    };

    private computeAggregates = (rotation: ShiftRotation, shiftsByCode: Map<string, Shift>): ShiftRotationAggregates => {
        let workDaysCount = 0;
        let restDaysCount = 0;
        let totalHours = 0;
        for (const day of rotation.days) {
            if (day.type !== ShiftRotationDayType.WORK) {
                restDaysCount++;
                continue;
            }
            workDaysCount++;
            for (const dayShift of day.shifts) {
                const shift = shiftsByCode.get(dayShift.shiftCode);
                if (!shift) continue;
                for (const slot of shift.timeSlots) totalHours += timeSlotHours(slot.startTime, slot.endTime);
            }
        }
        return { cycleLength: rotation.days.length, restDaysCount, totalHours, workDaysCount };
    };

    private assertNoTimeOverlap = (params: CreateOrUpdateShiftRotationVC): void => {
        const intervals: { dayPosition: number; shiftIndexInDay: number; start: number; end: number }[] = [];
        for (let i = 0; i < params.days.length; i++) {
            const day = params.days[i];
            if (day.type !== ShiftRotationDayType.WORK) continue;
            for (let shiftIdx = 0; shiftIdx < day.shifts.length; shiftIdx++) {
                const s = day.shifts[shiftIdx];
                for (const [startTime, endTime] of s.timeSlots) {
                    const start = i * MINUTES_PER_DAY + toMinutes(startTime);
                    const rawEnd = i * MINUTES_PER_DAY + toMinutes(endTime);
                    const end = rawEnd <= start ? rawEnd + MINUTES_PER_DAY : rawEnd;
                    intervals.push({ dayPosition: i + 1, end, shiftIndexInDay: shiftIdx, start });
                }
            }
        }

        for (let a = 0; a < intervals.length; a++) {
            for (let b = a + 1; b < intervals.length; b++) {
                const A = intervals[a];
                const B = intervals[b];
                if (intervalsOverlap(A.start, A.end, B.start, B.end)) {
                    const dayPositions = [Math.min(A.dayPosition, B.dayPosition), Math.max(A.dayPosition, B.dayPosition)];
                    throw new BadRequestError(`Shifts overlap`, ErrorCodes.SHIFT_ROTATION_OVERLAPPING_SHIFTS, {
                        dayPositions,
                        overlappingShifts: [
                            { dayPosition: A.dayPosition, shiftIndexInDay: A.shiftIndexInDay },
                            { dayPosition: B.dayPosition, shiftIndexInDay: B.shiftIndexInDay },
                        ],
                    });
                }
            }
        }
    };
}
