<?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\TaskAssignment;
use InvalidArgumentException;
use PDO;

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

    /** @return list<TaskAssignment> rows for a single task */
    public function allForTask(int $taskId): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM task_assignments WHERE task_id = ?'
        );
        $stmt->execute([$taskId]);
        $out = [];
        foreach ($stmt as $row) {
            $out[] = self::hydrate($row);
        }
        return $out;
    }

    /**
     * All assignments for a given sprint_worker (used by removeWorker to
     * snapshot rows before the FK cascade wipes them).
     *
     * @return list<TaskAssignment>
     */
    public function allForSprintWorker(int $swId): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM task_assignments WHERE sprint_worker_id = ?'
        );
        $stmt->execute([$swId]);
        $out = [];
        foreach ($stmt as $row) {
            $out[] = self::hydrate($row);
        }
        return $out;
    }

    public function find(int $taskId, int $swId): ?TaskAssignment
    {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM task_assignments WHERE task_id = ? AND sprint_worker_id = ?'
        );
        $stmt->execute([$taskId, $swId]);
        $row = $stmt->fetch();
        return is_array($row) ? self::hydrate($row) : null;
    }

    /**
     * Full grid for a sprint: [task_id][sw_id] => days. Tasks with no rows
     * are simply absent from the outer map.
     *
     * @return array<int, array<int, float>>
     */
    public function gridForSprint(int $sprintId): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT ta.task_id, ta.sprint_worker_id, ta.days
             FROM task_assignments ta
             JOIN tasks t ON t.id = ta.task_id
             WHERE t.sprint_id = ?'
        );
        $stmt->execute([$sprintId]);
        $out = [];
        foreach ($stmt as $row) {
            $tid = (int)   $row['task_id'];
            $sw  = (int)   $row['sprint_worker_id'];
            $d   = (float) $row['days'];
            $out[$tid][$sw] = $d;
        }
        return $out;
    }

    /**
     * Status grid for a sprint: [task_id][sw_id] => status. Mirrors
     * gridForSprint(); cells with no row are absent (default to
     * STATUS_ZUGEWIESEN at the call site).
     *
     * @return array<int, array<int, string>>
     */
    public function statusGridForSprint(int $sprintId): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT ta.task_id, ta.sprint_worker_id, ta.status
             FROM task_assignments ta
             JOIN tasks t ON t.id = ta.task_id
             WHERE t.sprint_id = ?'
        );
        $stmt->execute([$sprintId]);
        $out = [];
        foreach ($stmt as $row) {
            $tid = (int) $row['task_id'];
            $sw  = (int) $row['sprint_worker_id'];
            $out[$tid][$sw] = (string) $row['status'];
        }
        return $out;
    }

    /**
     * Σ task_assignments.days where task.priority = 1, grouped by sprint_worker_id.
     * Used by CapacityCalculator via SprintController::show.
     *
     * @return array<int, float>
     */
    public function committedPrio1BySprint(int $sprintId): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT ta.sprint_worker_id, SUM(ta.days) AS committed
             FROM task_assignments ta
             JOIN tasks t ON t.id = ta.task_id
             WHERE t.sprint_id = ? AND t.priority = 1
             GROUP BY ta.sprint_worker_id'
        );
        $stmt->execute([$sprintId]);
        $out = [];
        foreach ($stmt as $row) {
            $out[(int) $row['sprint_worker_id']] = (float) $row['committed'];
        }
        return $out;
    }

    /**
     * Set days for one (task, sprint_worker) cell with the same four-case
     * semantics as SprintWorkerDayRepository::upsert:
     *   - empty cell, days=0   -> NOOP
     *   - empty cell, days>0   -> CREATE (status seeded with default)
     *   - existing, unchanged  -> NOOP
     *   - existing, changed    -> UPDATE (row kept when zeroed; status preserved)
     *
     * @return array{action:string, before: ?TaskAssignment, after: ?TaskAssignment}
     */
    public function upsert(int $taskId, int $swId, float $days): array
    {
        $existing = $this->find($taskId, $swId);

        if ($existing !== null && abs($existing->days - $days) < 1e-9) {
            return ['action' => 'NOOP', 'before' => $existing, 'after' => $existing];
        }

        if ($existing === null) {
            if (abs($days) < 1e-9) {
                return ['action' => 'NOOP', 'before' => null, 'after' => null];
            }
            $stmt = $this->pdo->prepare(
                'INSERT INTO task_assignments (task_id, sprint_worker_id, days) VALUES (?, ?, ?)'
            );
            $stmt->execute([$taskId, $swId, $days]);
            $id    = (int) $this->pdo->lastInsertId();
            $after = new TaskAssignment($id, $taskId, $swId, $days, TaskAssignment::STATUS_ZUGEWIESEN);
            return ['action' => 'CREATE', 'before' => null, 'after' => $after];
        }

        $stmt = $this->pdo->prepare('UPDATE task_assignments SET days = ? WHERE id = ?');
        $stmt->execute([$days, $existing->id]);
        $after = new TaskAssignment($existing->id, $taskId, $swId, $days, $existing->status);
        return ['action' => 'UPDATE', 'before' => $existing, 'after' => $after];
    }

    /**
     * Set status for one (task, sprint_worker) cell. Creates a row with
     * days=0 when none exists so a state can be tracked even before any
     * days are assigned.
     *
     * @return array{action:string, before: ?TaskAssignment, after: ?TaskAssignment}
     */
    public function upsertStatus(int $taskId, int $swId, string $status): array
    {
        if (!TaskAssignment::isValidStatus($status)) {
            throw new InvalidArgumentException("invalid status: {$status}");
        }

        $existing = $this->find($taskId, $swId);

        if ($existing !== null && $existing->status === $status) {
            return ['action' => 'NOOP', 'before' => $existing, 'after' => $existing];
        }

        if ($existing === null) {
            // Default status on a fresh row would be 'zugewiesen', so writing
            // that is a no-op — saves an audit row for cells that never had
            // a non-default state.
            if ($status === TaskAssignment::STATUS_ZUGEWIESEN) {
                return ['action' => 'NOOP', 'before' => null, 'after' => null];
            }
            $stmt = $this->pdo->prepare(
                'INSERT INTO task_assignments (task_id, sprint_worker_id, days, status)
                 VALUES (?, ?, 0, ?)'
            );
            $stmt->execute([$taskId, $swId, $status]);
            $id    = (int) $this->pdo->lastInsertId();
            $after = new TaskAssignment($id, $taskId, $swId, 0.0, $status);
            return ['action' => 'CREATE', 'before' => null, 'after' => $after];
        }

        $stmt = $this->pdo->prepare('UPDATE task_assignments SET status = ? WHERE id = ?');
        $stmt->execute([$status, $existing->id]);
        $after = new TaskAssignment(
            $existing->id, $taskId, $swId, $existing->days, $status,
        );
        return ['action' => 'UPDATE', 'before' => $existing, 'after' => $after];
    }

    /**
     * @param array<string,mixed> $row
     */
    private static function hydrate(array $row): TaskAssignment
    {
        return new TaskAssignment(
            id:              (int)    $row['id'],
            taskId:          (int)    $row['task_id'],
            sprintWorkerId:  (int)    $row['sprint_worker_id'],
            days:            (float)  $row['days'],
            status:          (string) ($row['status'] ?? TaskAssignment::STATUS_ZUGEWIESEN),
        );
    }
}
