<?php
declare(strict_types=1);

namespace App\Services;

use App\Core\Audit\AuditLogger;
use App\Core\Database;
use PDO;
use RuntimeException;
use Throwable;

final class ReleaseMilestoneService
{
    private const STATUSES = ['planned', 'in_progress', 'completed', 'cancelled'];

    public function listForTrain(int $trainId, int $organisationId, bool $includeArchived = false): array
    {
        $this->requireTrain($trainId, $organisationId);
        $sql = 'SELECT m.* FROM axiom_release_milestones m '
            . 'WHERE m.release_train_id = :train_id AND m.organisation_id = :organisation_id ';
        if (!$includeArchived) {
            $sql .= 'AND m.archived_at IS NULL ';
        }
        $sql .= 'ORDER BY m.position ASC, m.planned_end_at ASC, m.id ASC';
        $statement = Database::connection()->prepare($sql);
        $statement->execute(['train_id' => $trainId, 'organisation_id' => $organisationId]);
        return $statement->fetchAll(PDO::FETCH_ASSOC);
    }

    public function find(int $id, int $trainId, int $organisationId, bool $includeArchived = false): ?array
    {
        $sql = 'SELECT * FROM axiom_release_milestones '
            . 'WHERE id = :id AND release_train_id = :train_id AND organisation_id = :organisation_id ';
        if (!$includeArchived) {
            $sql .= 'AND archived_at IS NULL';
        }
        $statement = Database::connection()->prepare($sql);
        $statement->execute(['id' => $id, 'train_id' => $trainId, 'organisation_id' => $organisationId]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    public function save(array $input, ?int $id, int $trainId, int $actorId, int $organisationId): int
    {
        $this->requireTrain($trainId, $organisationId);
        $title = trim((string) ($input['milestone_title'] ?? ''));
        $description = trim((string) ($input['milestone_description'] ?? ''));
        $status = (string) ($input['milestone_status'] ?? 'planned');
        $progress = max(0, min(100, (int) ($input['milestone_progress'] ?? 0)));
        $position = max(1, min(9999, (int) ($input['milestone_position'] ?? 10)));
        $plannedStart = $this->dateOrNull($input['milestone_planned_start_at'] ?? null);
        $plannedEnd = $this->dateOrNull($input['milestone_planned_end_at'] ?? null);
        $actualStart = $this->dateOrNull($input['milestone_actual_start_at'] ?? null);
        $actualEnd = $this->dateOrNull($input['milestone_actual_end_at'] ?? null);
        $ownerId = $this->positiveIntOrNull($input['milestone_owner_user_id'] ?? null);

        if ($title === '' || mb_strlen($title) > 180) {
            throw new RuntimeException('Titre du jalon obligatoire (180 caractères maximum).');
        }
        if (mb_strlen($description) > 5000) {
            throw new RuntimeException('Description du jalon trop longue.');
        }
        if (!in_array($status, self::STATUSES, true)) {
            throw new RuntimeException('Statut du jalon invalide.');
        }
        if ($plannedStart && $plannedEnd && $plannedEnd < $plannedStart) {
            throw new RuntimeException('La fin prévisionnelle doit être postérieure au début.');
        }
        if ($actualStart && $actualEnd && $actualEnd < $actualStart) {
            throw new RuntimeException('La fin réelle doit être postérieure au début réel.');
        }
        if ($status === 'completed') {
            $progress = 100;
            $actualEnd ??= date('Y-m-d H:i:s');
        }

        $parameters = [
            'train_id' => $trainId,
            'organisation_id' => $organisationId,
            'title' => $title,
            'description' => $description,
            'status' => $status,
            'progress' => $progress,
            'position' => $position,
            'planned_start_at' => $plannedStart,
            'planned_end_at' => $plannedEnd,
            'actual_start_at' => $actualStart,
            'actual_end_at' => $actualEnd,
            'owner_user_id' => $ownerId,
            'actor_id' => $actorId ?: null,
        ];
        $pdo = Database::connection();
        if ($id !== null) {
            if ($this->find($id, $trainId, $organisationId) === null) {
                throw new RuntimeException('Jalon introuvable.');
            }
            $parameters['id'] = $id;
            $statement = $pdo->prepare(
                'UPDATE axiom_release_milestones SET title = :title, description = :description, '
                . 'status = :status, progress = :progress, position = :position, '
                . 'planned_start_at = :planned_start_at, planned_end_at = :planned_end_at, '
                . 'actual_start_at = :actual_start_at, actual_end_at = :actual_end_at, '
                . 'owner_user_id = :owner_user_id, updated_by = :actor_id, updated_at = NOW() '
                . 'WHERE id = :id AND release_train_id = :train_id '
                . 'AND organisation_id = :organisation_id AND archived_at IS NULL'
            );
            $statement->execute($parameters);
            $this->audit('release_milestone.updated', $id, $actorId, $title);
            return $id;
        }

        $statement = $pdo->prepare(
            'INSERT INTO axiom_release_milestones '
            . '(organisation_id, release_train_id, title, description, status, progress, position, '
            . 'planned_start_at, planned_end_at, actual_start_at, actual_end_at, owner_user_id, '
            . 'created_by, updated_by, created_at, updated_at) '
            . 'VALUES (:organisation_id, :train_id, :title, :description, :status, :progress, :position, '
            . ':planned_start_at, :planned_end_at, :actual_start_at, :actual_end_at, :owner_user_id, '
            . ':actor_id, :actor_id, NOW(), NOW())'
        );
        $statement->execute($parameters);
        $newId = (int) $pdo->lastInsertId();
        $this->audit('release_milestone.created', $newId, $actorId, $title);
        return $newId;
    }

    public function archive(int $id, int $trainId, int $actorId, int $organisationId): void
    {
        $milestone = $this->find($id, $trainId, $organisationId);
        if ($milestone === null) {
            throw new RuntimeException('Jalon introuvable.');
        }
        $statement = Database::connection()->prepare(
            'UPDATE axiom_release_milestones SET archived_at = NOW(), updated_by = :actor_id, updated_at = NOW() '
            . 'WHERE id = :id AND release_train_id = :train_id '
            . 'AND organisation_id = :organisation_id AND archived_at IS NULL'
        );
        $statement->execute([
            'actor_id' => $actorId ?: null,
            'id' => $id,
            'train_id' => $trainId,
            'organisation_id' => $organisationId,
        ]);
        $this->audit('release_milestone.archived', $id, $actorId, (string) $milestone['title']);
    }

    public function restore(int $id, int $trainId, int $actorId, int $organisationId): void
    {
        $milestone = $this->find($id, $trainId, $organisationId, true);
        if ($milestone === null || $milestone['archived_at'] === null) {
            throw new RuntimeException('Jalon archivé introuvable.');
        }
        $statement = Database::connection()->prepare(
            'UPDATE axiom_release_milestones SET archived_at = NULL, updated_by = :actor_id, updated_at = NOW() '
            . 'WHERE id = :id AND release_train_id = :train_id '
            . 'AND organisation_id = :organisation_id AND archived_at IS NOT NULL'
        );
        $statement->execute([
            'actor_id' => $actorId ?: null,
            'id' => $id,
            'train_id' => $trainId,
            'organisation_id' => $organisationId,
        ]);
        $this->audit('release_milestone.restored', $id, $actorId, (string) $milestone['title']);
    }

    private function requireTrain(int $trainId, int $organisationId): void
    {
        $statement = Database::connection()->prepare(
            'SELECT id FROM axiom_release_trains '
            . 'WHERE id = :id AND organisation_id = :organisation_id AND archived_at IS NULL'
        );
        $statement->execute(['id' => $trainId, 'organisation_id' => $organisationId]);
        if (!$statement->fetchColumn()) {
            throw new RuntimeException('Release train introuvable.');
        }
    }

    private function dateOrNull(mixed $value): ?string
    {
        $value = trim((string) $value);
        if ($value === '') {
            return null;
        }
        $timestamp = strtotime($value);
        if ($timestamp === false) {
            throw new RuntimeException('Date invalide.');
        }
        return date('Y-m-d H:i:s', $timestamp);
    }

    private function positiveIntOrNull(mixed $value): ?int
    {
        $integer = (int) $value;
        return $integer > 0 ? $integer : null;
    }

    private function audit(string $event, int $id, int $actorId, string $details): void
    {
        try {
            (new AuditLogger())->record(
                $event,
                'release_train_manager',
                'release_milestone',
                $id,
                null,
                ['actor_id' => $actorId ?: null, 'details' => $details]
            );
        } catch (Throwable) {
        }
    }
}
