<?php

namespace App\Services;

use App\Models\Project;
use App\Models\ProjectStatus;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

/**
 * Project Service
 *
 * Handles business logic for project operations.
 */
class ProjectService
{
    public function __construct(protected ProjectStatusService $statusService) {}

    /**
     * Get all projects with optional filtering.
     *
     * @param  int|null  $statusId  Filter by status ID
     * @param  int|null  $typeId  Filter by type ID
     * @return Collection<Project>
     */
    public function getAll(?int $statusId = null, ?int $typeId = null): Collection
    {
        $query = Project::with([
            'status:id,name,order',
            'type:id,name',
        ])
            ->select('projects.*')
            ->leftJoin('project_statuses', 'projects.status_id', '=', 'project_statuses.id');

        if ($statusId !== null) {
            $query->where('projects.status_id', $statusId);
        }

        if ($typeId !== null) {
            $query->where('projects.type_id', $typeId);
        }

        return $query->get();
    }

    /**
     * Find a project by ID.
     */
    public function findById(string $id): ?Project
    {
        return Project::with(['status', 'type', 'allocations', 'actuals'])->find($id);
    }

    /**
     * Create a new project.
     *
     * @throws ValidationException
     */
    public function create(array $data): Project
    {
        $validator = Validator::make($data, [
            'code' => 'required|string|max:50|unique:projects,code',
            'title' => 'required|string|max:255',
            'type_id' => 'required|integer|exists:project_types,id',
            'status_id' => 'sometimes|integer|exists:project_statuses,id',
        ], [
            'code.unique' => 'Project code must be unique',
        ]);

        if ($validator->fails()) {
            throw new ValidationException($validator);
        }

        // Default to first status (Pre-sales) if not provided
        if (! isset($data['status_id'])) {
            $initialStatus = ProjectStatus::orderBy('order')->first();
            $data['status_id'] = $initialStatus?->id;
        }

        $project = Project::create($data);
        $project->load(['status', 'type']);

        return $project;
    }

    /**
     * Update an existing project.
     *
     * @throws ValidationException
     */
    public function update(Project $project, array $data): Project
    {
        $validator = Validator::make($data, [
            'code' => 'sometimes|string|max:50|unique:projects,code,'.$project->id,
            'title' => 'sometimes|string|max:255',
            'type_id' => 'sometimes|integer|exists:project_types,id',
            'status_id' => 'sometimes|integer|exists:project_statuses,id',
            'approved_estimate' => 'sometimes|numeric|min:0',
        ], [
            'code.unique' => 'Project code must be unique',
        ]);

        if ($validator->fails()) {
            throw new ValidationException($validator);
        }

        $project->update($data);
        $project->load(['status', 'type']);

        return $project;
    }

    /**
     * Transition project to a new status.
     *
     * @throws \RuntimeException
     */
    public function transitionStatus(Project $project, int $newStatusId): Project
    {
        $newStatus = ProjectStatus::find($newStatusId);

        if (! $newStatus) {
            throw new \RuntimeException('Invalid status', 422);
        }

        $currentStatusName = $project->status->name;
        $newStatusName = $newStatus->name;

        // Check if transition is valid
        if (! $this->statusService->canTransition($currentStatusName, $newStatusName)) {
            throw new \RuntimeException(
                "Cannot transition from {$currentStatusName} to {$newStatusName}",
                422
            );
        }

        // Special validation: Estimate Approved requires approved_estimate > 0
        if ($this->statusService->requiresEstimate($newStatusName)) {
            if (! $project->approved_estimate || $project->approved_estimate <= 0) {
                throw new \RuntimeException(
                    'Cannot transition to Estimate Approved without an approved estimate',
                    422
                );
            }
        }

        $project->update(['status_id' => $newStatusId]);
        $project->load(['status', 'type']);

        return $project;
    }

    /**
     * Set the approved estimate for a project.
     *
     * @throws \RuntimeException
     */
    public function setApprovedEstimate(Project $project, float $estimate): Project
    {
        if ($estimate <= 0) {
            throw new \RuntimeException('Approved estimate must be greater than 0', 422);
        }

        $project->update(['approved_estimate' => $estimate]);
        $project->load(['status', 'type']);

        return $project;
    }

    /**
     * Set the forecasted effort for a project.
     *
     * @param  array  $forecastedEffort  ['2024-01' => 40, '2024-02' => 60, ...]
     *
     * @throws \RuntimeException
     */
    public function setForecastedEffort(Project $project, array $forecastedEffort): Project
    {
        // Calculate total forecasted hours
        $totalForecasted = array_sum($forecastedEffort);

        // If project has approved estimate, validate within tolerance
        if ($project->approved_estimate && $project->approved_estimate > 0) {
            $approved = (float) $project->approved_estimate;
            $difference = $totalForecasted - $approved;
            $percentageDiff = ($difference / $approved) * 100;
            $tolerancePercent = 5;

            if (abs($percentageDiff) > $tolerancePercent) {
                $lowerBound = max(0, round($approved * (1 - $tolerancePercent / 100), 2));
                $upperBound = round($approved * (1 + $tolerancePercent / 100), 2);
                $message = sprintf(
                    'Forecasted effort (%s h) %s approved estimate (%s h) by %s hours (%s%%). Forecasted effort must be between %s and %s hours for a %s hour estimate.',
                    number_format($totalForecasted, 2, '.', ''),
                    $difference > 0 ? 'exceeds' : 'is below',
                    number_format($approved, 2, '.', ''),
                    number_format(abs($difference), 2, '.', ''),
                    number_format(abs($percentageDiff), 2, '.', ''),
                    number_format($lowerBound, 2, '.', ''),
                    number_format($upperBound, 2, '.', ''),
                    number_format($approved, 2, '.', '')
                );

                throw new \RuntimeException($message, 422);
            }
        }

        $project->update(['forecasted_effort' => $forecastedEffort]);
        $project->load(['status', 'type']);

        return $project;
    }

    /**
     * Check if a project can be deleted.
     *
     * @return array{canDelete: bool, reason?: string}
     */
    public function canDelete(Project $project): array
    {
        if ($project->allocations()->exists()) {
            return [
                'canDelete' => false,
                'reason' => 'Project has allocations',
            ];
        }

        if ($project->actuals()->exists()) {
            return [
                'canDelete' => false,
                'reason' => 'Project has actuals',
            ];
        }

        return ['canDelete' => true];
    }
}
