<?php
/*
 * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * See the LICENSE file in the project root for the full license text.
 */

declare(strict_types=1);

namespace App\Repositories;

use App\Domain\SprintWeek;
use DateTimeImmutable;
use PDO;
use RuntimeException;

final class SprintWeekRepository
{
    public function __construct(private readonly PDO $pdo)
    {
    }

    /** @return list<SprintWeek> ordered by sort_order ASC */
    public function allForSprint(int $sprintId): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM sprint_weeks WHERE sprint_id = ? ORDER BY sort_order ASC'
        );
        $stmt->execute([$sprintId]);
        $out = [];
        foreach ($stmt as $row) {
            $out[] = self::hydrate($row);
        }
        return $out;
    }

    public function find(int $id): ?SprintWeek
    {
        $stmt = $this->pdo->prepare('SELECT * FROM sprint_weeks WHERE id = ?');
        $stmt->execute([$id]);
        $row = $stmt->fetch();
        return is_array($row) ? self::hydrate($row) : null;
    }

    /**
     * Update the weekday selection mask for a week. `max_working_days` is
     * recomputed as `popcount(mask)` and written atomically with the mask —
     * the two columns are never out of sync.
     *
     * @return array{before: SprintWeek, after: SprintWeek}
     */
    public function updateActiveDays(int $weekId, int $mask): array
    {
        $before = $this->find($weekId);
        if ($before === null) {
            throw new RuntimeException("sprint_week {$weekId} not found");
        }
        $mask &= SprintWeek::MASK_ALL;
        $count = (float) SprintWeek::popcount($mask);
        $this->pdo
            ->prepare('UPDATE sprint_weeks SET active_days_mask = ?, max_working_days = ? WHERE id = ?')
            ->execute([$mask, $count, $weekId]);
        $after = $this->find($weekId) ?? $before;
        return ['before' => $before, 'after' => $after];
    }

    /**
     * Realign existing weeks' `start_date` / `iso_week` so that week i
     * (1-indexed by `sort_order`) starts at `$sprintStartDate + (i-1)*7 days`.
     * `active_days_mask` and `max_working_days` are preserved.
     *
     * Returns before/after pairs only for rows that actually changed, so the
     * caller can audit each UPDATE.
     *
     * @return list<array{before: SprintWeek, after: SprintWeek}>
     */
    public function realignDates(int $sprintId, string $sprintStartDate): array
    {
        $d0 = DateTimeImmutable::createFromFormat('Y-m-d', $sprintStartDate);
        if ($d0 === false) {
            throw new RuntimeException("Invalid sprintStartDate: {$sprintStartDate}");
        }

        $existing = $this->allForSprint($sprintId);
        if ($existing === []) {
            return [];
        }

        $update = $this->pdo->prepare(
            'UPDATE sprint_weeks SET iso_week = ?, start_date = ? WHERE id = ?'
        );

        $diffs = [];
        foreach ($existing as $w) {
            $expectedStart = $d0->modify('+' . ($w->sortOrder - 1) . ' weeks');
            $ymd = $expectedStart->format('Y-m-d');
            $iso = (int) $expectedStart->format('W');
            if ($w->startDate === $ymd && $w->isoWeek === $iso) {
                continue;
            }
            $update->execute([$iso, $ymd, $w->id]);
            $after = new SprintWeek(
                id:              $w->id,
                sprintId:        $w->sprintId,
                sortOrder:       $w->sortOrder,
                isoWeek:         $iso,
                startDate:       $ymd,
                maxWorkingDays:  $w->maxWorkingDays,
                activeDaysMask:  $w->activeDaysMask,
            );
            $diffs[] = ['before' => $w, 'after' => $after];
        }
        return $diffs;
    }

    /**
     * Resize the week set of a sprint to $targetCount weeks.
     *
     * - Added rows get max_working_days=5 and dates offset +7 days per week
     *   from the sprint start.
     * - Removed rows are the trailing ones; any sprint_worker_days attached
     *   to them cascade-delete via the FK.
     *
     * Returns the before/after diff for auditing.
     *
     * @return array{added: list<SprintWeek>, removed: list<SprintWeek>}
     */
    public function syncCount(
        int $sprintId,
        string $sprintStartDate,
        int $targetCount,
    ): array {
        if ($targetCount < 1) {
            throw new RuntimeException("targetCount must be >= 1, got {$targetCount}");
        }

        $existing = $this->allForSprint($sprintId);
        $currentCount = count($existing);

        if ($targetCount === $currentCount) {
            return ['added' => [], 'removed' => []];
        }

        if ($targetCount < $currentCount) {
            // Drop the trailing rows by sort_order.
            $toRemove = array_slice($existing, $targetCount);
            $ids = array_map(fn(SprintWeek $w) => $w->id, $toRemove);
            $placeholders = implode(',', array_fill(0, count($ids), '?'));
            $this->pdo
                ->prepare("DELETE FROM sprint_weeks WHERE id IN ({$placeholders})")
                ->execute($ids);
            return ['added' => [], 'removed' => $toRemove];
        }

        // Append rows.
        $d0 = DateTimeImmutable::createFromFormat('Y-m-d', $sprintStartDate);
        if ($d0 === false) {
            throw new RuntimeException("Invalid sprintStartDate: {$sprintStartDate}");
        }

        $insert = $this->pdo->prepare(
            'INSERT INTO sprint_weeks
             (sprint_id, sort_order, iso_week, start_date, max_working_days, active_days_mask)
             VALUES (?, ?, ?, ?, ?, ?)'
        );
        $added = [];
        for ($i = $currentCount + 1; $i <= $targetCount; $i++) {
            $weekStart = $d0->modify('+' . ($i - 1) . ' weeks');
            $iso       = (int) $weekStart->format('W');
            $ymd       = $weekStart->format('Y-m-d');
            $insert->execute([$sprintId, $i, $iso, $ymd, 5.0, SprintWeek::MASK_ALL]);
            $added[] = new SprintWeek(
                id:              (int) $this->pdo->lastInsertId(),
                sprintId:        $sprintId,
                sortOrder:       $i,
                isoWeek:         $iso,
                startDate:       $ymd,
                maxWorkingDays:  5.0,
                activeDaysMask:  SprintWeek::MASK_ALL,
            );
        }
        return ['added' => $added, 'removed' => []];
    }

    /**
     * @param array<string,mixed> $row
     */
    private static function hydrate(array $row): SprintWeek
    {
        return new SprintWeek(
            id:             (int)    $row['id'],
            sprintId:       (int)    $row['sprint_id'],
            sortOrder:      (int)    $row['sort_order'],
            isoWeek:        (int)    $row['iso_week'],
            startDate:      (string) $row['start_date'],
            maxWorkingDays: (float)  $row['max_working_days'],
            activeDaysMask: (int)    ($row['active_days_mask'] ?? SprintWeek::MASK_ALL),
        );
    }
}
