<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\ActualResource;
use App\Models\Actual;
use App\Models\Allocation;
use App\Models\Project;
use App\Models\TeamMember;
use App\Services\ActualsService;
use Carbon\Carbon;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Pagination\LengthAwarePaginator;

class ActualController extends Controller
{
    use AuthorizesRequests;
    protected ActualsService $actualsService;

    private const MAX_PER_PAGE = 250;
    private const MAX_HOURS_PER_ENTRY = 744; // 24h * 31 days - maximum hours in a month
    private const VARIANCE_GREEN_THRESHOLD = 5;
    private const VARIANCE_YELLOW_THRESHOLD = 20;

    public function __construct(ActualsService $actualsService)
    {
        $this->actualsService = $actualsService;
    }

    public function index(Request $request): JsonResponse
    {
        $validator = Validator::make($request->all(), [
            'month' => ['required', 'date_format:Y-m'],
            'project_ids.*' => ['uuid'],
            'team_member_ids.*' => ['uuid'],
            'include_inactive' => ['nullable', 'in:true,false,1,0'],
            'page' => ['integer', 'min:1'],
            'per_page' => ['integer', 'min:1', 'max:' . self::MAX_PER_PAGE],
        ]);

        if ($validator->fails()) {
            return response()->json([
                'message' => 'Validation failed',
                'errors' => $validator->errors(),
            ], 422);
        }

        $monthKey = $request->query('month');

        try {
            $monthDate = Carbon::createFromFormat('Y-m', $monthKey)->startOfMonth()->toDateString();
        } catch (\Throwable) {
            return response()->json([
                'message' => 'Validation failed',
                'errors' => ['month' => ['Invalid month format']],
            ], 422);
        }

        $includeInactive = filter_var($request->query('include_inactive', false), FILTER_VALIDATE_BOOLEAN);
        $projectIdsFilter = array_filter((array) $request->query('project_ids', []));
        $teamMemberIdsFilter = array_filter((array) $request->query('team_member_ids', []));
        $searchTerm = $request->query('search');
        $searchTerm = is_string($searchTerm) ? trim($searchTerm) : null;
        if ($searchTerm === '') {
            $searchTerm = null;
        }

        // Escape LIKE wildcards to prevent SQL injection via pattern matching
        $escapedSearchTerm = $searchTerm !== null
            ? str_replace(['%', '_', '\\'], ['\\%', '\\_', '\\\\'], $searchTerm)
            : null;

        $inactiveStatuses = $this->actualsService->getInactiveProjectStatuses();

        $projects = Project::with('status')
            ->when($projectIdsFilter, fn ($query) => $query->whereIn('id', $projectIdsFilter))
            ->when(! $includeInactive, fn ($query) => $query->whereHas('status', fn ($query) => $query->whereNotIn('name', $inactiveStatuses)))
            ->when($escapedSearchTerm !== null, fn ($query) => $query->where(fn ($query) => $query->where('code', 'like', "%{$escapedSearchTerm}%")->orWhere('title', 'like', "%{$escapedSearchTerm}%")))
            ->orderBy('code')
            ->get();

        $teamMembers = TeamMember::query()
            ->when($teamMemberIdsFilter, fn ($query) => $query->whereIn('id', $teamMemberIdsFilter))
            ->when(! $includeInactive, fn ($query) => $query->where('active', true))
            ->orderBy('name')
            ->get();

        $allocations = collect();
        $actuals = collect();

        $projectIds = $projects->pluck('id')->all();
        $teamMemberIds = $teamMembers->pluck('id')->all();

        if (! empty($projectIds) && ! empty($teamMemberIds)) {
            $allocations = Allocation::query()
                ->whereDate('month', $monthDate)
                ->when($projectIds, fn ($query) => $query->whereIn('project_id', $projectIds))
                ->when($teamMemberIds, fn ($query) => $query->whereIn('team_member_id', $teamMemberIds))
                ->get()
                ->keyBy(fn (Allocation $allocation) => $allocation->project_id.'-'.$allocation->team_member_id);

            $actuals = Actual::query()
                ->whereDate('month', $monthDate)
                ->when($projectIds, fn ($query) => $query->whereIn('project_id', $projectIds))
                ->when($teamMemberIds, fn ($query) => $query->whereIn('team_member_id', $teamMemberIds))
                ->get()
                ->keyBy(fn (Actual $actual) => $actual->project_id.'-'.$actual->team_member_id);
        }

        $teamMemberMap = $teamMembers->mapWithKeys(fn (TeamMember $member) => [$member->id => $this->formatTeamMember($member)])->all();
        $rows = [];

        foreach ($projects as $project) {
            $projectData = $this->formatProject($project);

            foreach ($teamMembers as $teamMember) {
                $key = $project->id.'-'.$teamMember->id;
                $allocation = $allocations->get($key);
                $actual = $actuals->get($key);
                $hasAllocation = $allocation !== null;
                $hasActual = $actual !== null;
                $hasData = $hasAllocation || $hasActual;

                $allocatedHours = $hasAllocation ? (float) $allocation->allocated_hours : 0.0;
                $actualHours = $hasActual ? (float) $actual->hours_logged : 0.0;

                $variancePercentage = null;
                $varianceDisplay = null;

                if ($hasData) {
                    if ($allocatedHours <= 0.0) {
                        if ($actualHours > 0.0) {
                            $varianceDisplay = '∞%';
                        } else {
                            $variancePercentage = 0.0;
                        }
                    } else {
                        $variancePercentage = (($actualHours - $allocatedHours) / $allocatedHours) * 100.0;
                    }
                }

                $variance = $variancePercentage !== null ? round($variancePercentage, 1) : null;
                $indicator = $this->getIndicator($variancePercentage, $hasData);

                $rows[] = [
                    'project_id' => $project->id,
                    'project' => $projectData,
                    'team_member_id' => $teamMember->id,
                    'team_member' => $teamMemberMap[$teamMember->id],
                    'month' => $monthKey,
                    'allocated_hours' => $this->formatHours($allocatedHours),
                    'actual_hours' => $this->formatHours($actualHours),
                    'variance_percentage' => $variance,
                    'variance_display' => $varianceDisplay,
                    'variance_indicator' => $indicator,
                    'notes' => $actual?->notes,
                    'is_readonly' => $this->isProjectReadonly($project),
                ];
            }
        }

        $page = max(1, (int) $request->query('page', 1));
        $perPage = max(1, min(self::MAX_PER_PAGE, (int) $request->query('per_page', 25)));
        $total = count($rows);
        $currentPageItems = array_slice($rows, ($page - 1) * $perPage, $perPage);

        $paginator = new LengthAwarePaginator(
            $currentPageItems,
            $total,
            $perPage,
            $page,
            ['path' => LengthAwarePaginator::resolveCurrentPath(), 'query' => $request->query()]
        );

        return response()->json([
            'data' => $paginator->items(),
            'meta' => [
                'current_page' => $paginator->currentPage(),
                'per_page' => $paginator->perPage(),
                'total' => $paginator->total(),
                'last_page' => $paginator->lastPage(),
                'filters' => [
                    'month' => $monthKey,
                    'include_inactive' => $includeInactive,
                    'search' => $searchTerm,
                ],
            ],
        ]);
    }

    public function store(Request $request): JsonResponse
    {
        $validator = Validator::make($request->all(), [
            'project_id' => 'required|uuid|exists:projects,id',
            'team_member_id' => 'required|uuid|exists:team_members,id',
            'month' => 'required|date_format:Y-m',
            'hours' => 'required|numeric|min:0|max:' . self::MAX_HOURS_PER_ENTRY,
            'notes' => 'nullable|string|max:1000',
        ]);

        $validator->after(function () use ($request, $validator) {
            $month = $request->input('month');

            if (! $month) {
                return;
            }

            try {
                $monthDate = Carbon::createFromFormat('Y-m', $month)->startOfMonth();
            } catch (\Exception) {
                return;
            }

            if ($monthDate->gt(Carbon::now()->startOfMonth())) {
                $validator->errors()->add('month', 'Cannot log hours for future months');
            }
        });

        if ($validator->fails()) {
            return response()->json([
                'message' => 'Validation failed',
                'errors' => $validator->errors(),
            ], 422);
        }

        // Authorization check for creating actuals
        $this->authorize('create', [Actual::class, $request->input('team_member_id')]);

        $project = Project::with('status')->find($request->input('project_id'));

        if ($project && $project->status && ! $this->actualsService->canLogToInactiveProjects()) {
            $inactiveStatuses = $this->actualsService->getInactiveProjectStatuses();

            if (in_array($project->status->name, $inactiveStatuses, true)) {
                return response()->json([
                    'message' => 'Cannot log hours to completed projects',
                    'errors' => [
                        'project_id' => ['Cannot log hours to completed projects'],
                    ],
                ], 422);
            }
        }

        $monthKey = $request->input('month');
        $monthDate = Carbon::createFromFormat('Y-m', $monthKey)->startOfMonth()->toDateString();
        $hours = (float) $request->input('hours');
        $notes = $request->input('notes');

        $status = 201;
        $actual = null;

        DB::transaction(function () use ($request, $monthDate, $hours, $notes, &$status, &$actual) {
            $existing = Actual::where('project_id', $request->input('project_id'))
                ->where('team_member_id', $request->input('team_member_id'))
                ->whereDate('month', $monthDate)
                ->lockForUpdate()
                ->first();

            if ($existing) {
                // Use atomic increment to prevent race conditions
                DB::table('actuals')
                    ->where('id', $existing->id)
                    ->increment('hours_logged', $hours);

                if ($notes) {
                    $existing->notes = $this->appendNotes($existing->notes, $notes);
                    $existing->save();
                }

                $existing->refresh();
                $actual = $existing;
                $status = 200;
            } else {
                $actual = Actual::create([
                    'project_id' => $request->input('project_id'),
                    'team_member_id' => $request->input('team_member_id'),
                    'month' => $monthDate,
                    'hours_logged' => $hours,
                    'notes' => $notes,
                ]);
            }
        });

        $actual->load(['project.status', 'teamMember']);
        $this->hydrateVariance($actual, $monthKey);

        return response()->json([
            'data' => (new ActualResource($actual))->resolve($request),
        ], $status);
    }

    public function show(string $id): JsonResponse
    {
        $actual = Actual::with(['project.status', 'teamMember'])->find($id);

        if (! $actual) {
            return response()->json([
                'message' => 'Actual not found',
            ], 404);
        }

        $monthKey = $actual->month?->format('Y-m');

        if ($monthKey) {
            $this->hydrateVariance($actual, $monthKey);
        }

        return $this->wrapResource(new ActualResource($actual));
    }

    public function update(Request $request, string $id): JsonResponse
    {
        $actual = Actual::find($id);

        if (! $actual) {
            return response()->json([
                'message' => 'Actual not found',
            ], 404);
        }

        // Authorization check for updating actuals
        $this->authorize('update', $actual);

        $validator = Validator::make($request->all(), [
            'hours' => 'required|numeric|min:0|max:' . self::MAX_HOURS_PER_ENTRY,
            'notes' => 'nullable|string|max:1000',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'message' => 'Validation failed',
                'errors' => $validator->errors(),
            ], 422);
        }

        $actual->hours_logged = (float) $request->input('hours');

        if ($request->exists('notes')) {
            $actual->notes = $request->input('notes');
        }

        $actual->save();
        $actual->load(['project.status', 'teamMember']);

        $monthKey = $actual->month?->format('Y-m');

        if ($monthKey) {
            $this->hydrateVariance($actual, $monthKey);
        }

        return $this->wrapResource(new ActualResource($actual));
    }

    public function destroy(string $id): JsonResponse
    {
        $actual = Actual::find($id);

        if (! $actual) {
            return response()->json([
                'message' => 'Actual not found',
            ], 404);
        }

        // Authorization check for deleting actuals
        $this->authorize('delete', $actual);

        $actual->delete();

        return response()->json([
            'message' => 'Actual deleted successfully',
        ]);
    }

    private function hydrateVariance(Actual $actual, string $month): void
    {
        $variance = $this->actualsService->calculateVariance(
            $actual->project_id,
            $actual->team_member_id,
            $month
        );

        $actual->allocated_hours = $variance['allocated'];
        $actual->variance_percentage = $variance['variance_percentage'];
        $actual->variance_indicator = $variance['indicator'];
    }

    private function formatProject(Project $project): array
    {
        $status = $project->status;

        return [
            'id' => $project->id,
            'code' => $project->code,
            'title' => $project->title,
            'status' => $status ? [
                'id' => $status->id,
                'name' => $status->name,
                'is_active' => (bool) $status->is_active,
            ] : null,
            'is_active' => $this->isProjectActive($project),
        ];
    }

    private function formatTeamMember(TeamMember $teamMember): array
    {
        return [
            'id' => $teamMember->id,
            'name' => $teamMember->name,
            'is_active' => (bool) $teamMember->active,
        ];
    }

    private function isProjectActive(Project $project): bool
    {
        return ! $this->isProjectReadonly($project);
    }

    private function isProjectReadonly(Project $project): bool
    {
        $status = $project->status;

        if (! $status) {
            return false;
        }

        return in_array($status->name, $this->actualsService->getInactiveProjectStatuses(), true);
    }

    private function formatHours(float $hours): string
    {
        return number_format($hours, 2, '.', '');
    }

    private function getIndicator(?float $variancePercentage, bool $hasData): string
    {
        if (! $hasData) {
            return 'gray';
        }

        if ($variancePercentage === null) {
            return 'red';
        }

        $absolute = abs($variancePercentage);

        if ($absolute <= self::VARIANCE_GREEN_THRESHOLD) {
            return 'green';
        }

        if ($absolute <= self::VARIANCE_YELLOW_THRESHOLD) {
            return 'yellow';
        }

        return 'red';
    }

    private function appendNotes(?string $existing, string $note): string
    {
        $entry = now()->format('Y-m-d H:i').': '.$note;

        if ($existing) {
            return $existing."\n".$entry;
        }

        return $entry;
    }
}
