<?php

namespace App\Http\Controllers;

use App\Events\ProjectTaskStageChanged;
use App\Models\Company;
use App\Models\Deal;
use App\Models\Project;
use App\Models\ProjectStage;
use App\Models\Task;
use App\Models\User;
use App\Support\CrmModuleManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;

class ProjectController extends Controller
{
    /**
     * @var array<string, string>
     */
    private const STATUS_LABELS = [
        'planned' => 'Planned',
        'active' => 'Active',
        'on_hold' => 'On hold',
        'completed' => 'Completed',
        'cancelled' => 'Cancelled',
    ];

    /**
     * @var list<string>
     */
    private const STAGE_COLOR_PALETTE = [
        '#3B82F6',
        '#0EA5E9',
        '#8B5CF6',
        '#F59E0B',
        '#14B8A6',
        '#6366F1',
        '#EC4899',
        '#94A3B8',
    ];

    public function __construct()
    {
        $this->authorizeResource(Project::class, 'project');
    }

    public function index(Request $request): View
    {
        /** @var User $user */
        $user = $request->user();

        $search = trim((string) $request->input('q', ''));
        $status = $request->input('status');
        $managerId = $request->integer('manager_id');

        $viewOptions = [
            'list' => __('List'),
            'kanban' => __('Kanban'),
        ];

        $preferredViewMode = (string) ($user->preferred_project_view ?? 'list');
        if (! array_key_exists($preferredViewMode, $viewOptions)) {
            $preferredViewMode = 'list';
        }

        if ($request->has('view')) {
            $requestedViewMode = (string) $request->input('view', '');
            if (array_key_exists($requestedViewMode, $viewOptions)) {
                $viewMode = $requestedViewMode;

                if ((string) $user->preferred_project_view !== $viewMode) {
                    $user->forceFill([
                        'preferred_project_view' => $viewMode,
                    ])->saveQuietly();
                }
            } else {
                $viewMode = 'list';
            }
        } else {
            $viewMode = $preferredViewMode;
        }

        $projectsQuery = Project::query()
            ->with(['company', 'deal', 'owner', 'manager'])
            ->withCount([
                'tasks',
                'tasks as open_tasks_count' => fn ($query) => $query->whereIn('status', ['todo', 'in_progress', 'review']),
                'tasks as done_tasks_count' => fn ($query) => $query->where('status', 'done'),
            ])
            ->when($search !== '', function ($query) use ($search): void {
                $query->where(function ($sub) use ($search): void {
                    $sub->where('name', 'like', "%{$search}%")
                        ->orWhere('code', 'like', "%{$search}%")
                        ->orWhereHas('company', fn ($company) => $company->where('name', 'like', "%{$search}%"))
                        ->orWhereHas('deal', fn ($deal) => $deal->where('title', 'like', "%{$search}%"));
                });
            })
            ->when($status, fn ($query) => $query->where('status', $status))
            ->when($managerId, fn ($query) => $query->where('manager_id', $managerId));

        $projects = null;
        $boardColumns = collect();

        if ($viewMode === 'kanban') {
            $groupedProjects = (clone $projectsQuery)
                ->orderByRaw('CASE WHEN due_at IS NULL THEN 1 ELSE 0 END')
                ->orderBy('due_at')
                ->orderByDesc('updated_at')
                ->get()
                ->groupBy('status');

            $boardColumns = collect($this->projectStatusOptions())->map(function (array $meta, string $statusKey) use ($groupedProjects): array {
                $columnProjects = $groupedProjects->get($statusKey, collect());

                return [
                    'status' => $statusKey,
                    'label' => $meta['label'],
                    'header_class' => $meta['header_class'],
                    'surface_style' => $meta['surface_style'],
                    'projects' => $columnProjects,
                    'count' => $columnProjects->count(),
                    'budget' => (float) $columnProjects->sum('budget'),
                ];
            })->values();
        } else {
            $projects = (clone $projectsQuery)
                ->latest()
                ->paginate(15)
                ->withQueryString();
        }

        $managers = User::query()->orderBy('name')->get();

        return view('projects.index', compact(
            'projects',
            'search',
            'status',
            'managerId',
            'managers',
            'viewOptions',
            'viewMode',
            'boardColumns'
        ));
    }

    public function create(Request $request): View
    {
        $defaults = [
            'project' => new Project([
                'company_id' => $request->input('company_id'),
                'deal_id' => $request->input('deal_id'),
                'status' => 'planned',
                'priority' => 'medium',
                'health' => 'normal',
                'visibility' => 'team',
            ]),
            'stagesInput' => "Backlog\nIn progress\nExamination\nReady",
            'selectedMembers' => collect(),
        ];

        return view('projects.create', [
            ...$this->formData(),
            ...$defaults,
        ]);
    }

    public function store(Request $request, CrmModuleManager $moduleManager): RedirectResponse
    {
        $payload = $this->validatedData($request);
        $payload = $moduleManager->applyPayloadHooks('projects.store', $payload, [
            'hook' => 'projects.store',
            'user_id' => $request->user()->id,
        ], array_keys($payload));

        $memberIds = $payload['members'];
        $stagesInput = $payload['stages'];
        unset($payload['members'], $payload['stages']);

        $payload['owner_id'] = $payload['owner_id'] ?: $request->user()->id;
        $payload['completed_at'] = $payload['status'] === 'completed' ? now() : null;

        $project = Project::create($payload);

        $this->syncStages($project, $stagesInput);
        $this->syncMembers($project, $memberIds, $project->owner_id, $project->manager_id);

        return redirect()
            ->route('projects.show', $project)
            ->with('success', 'The project has been created.');
    }

    public function show(Request $request, Project $project): View
    {
        /** @var User $user */
        $user = $request->user();

        $project->load([
            'company',
            'deal',
            'owner',
            'manager',
            'members',
            'stages',
            'tasks.assignee',
            'tasks.creator',
            'tasks.deal',
            'tasks.company',
            'tasks.contact',
        ]);

        $tasksByStage = $project->tasks
            ->sortBy([
                ['sort_order', 'asc'],
                ['due_at', 'asc'],
            ])
            ->groupBy('project_stage_id');

        $board = $project->stages->map(function (ProjectStage $stage) use ($tasksByStage): array {
            $tasks = $tasksByStage->get($stage->id, collect());

            return [
                'stage' => $stage,
                'tasks' => $tasks,
                'hours' => $tasks->sum('tracked_hours'),
            ];
        });

        $unassignedTasks = $tasksByStage->get(null, collect());
        $canManageTasks = $user->can('manageTasks', $project);

        return view('projects.show', compact('project', 'board', 'unassignedTasks', 'canManageTasks'));
    }

    public function edit(Project $project): View
    {
        $project->load(['stages' => fn ($query) => $query->orderBy('sort_order'), 'members']);

        $stagesInput = $project->stages->pluck('name')->implode(PHP_EOL);
        $selectedMembers = $project->members->pluck('id');

        return view('projects.edit', [
            ...$this->formData(),
            'project' => $project,
            'stagesInput' => $stagesInput,
            'selectedMembers' => $selectedMembers,
        ]);
    }

    public function update(Request $request, Project $project, CrmModuleManager $moduleManager): RedirectResponse
    {
        $payload = $this->validatedData($request, $project);
        $payload = $moduleManager->applyPayloadHooks('projects.update', $payload, [
            'hook' => 'projects.update',
            'user_id' => $request->user()->id,
            'project_id' => $project->id,
        ], array_keys($payload));

        $memberIds = $payload['members'];
        $stagesInput = $payload['stages'];
        unset($payload['members'], $payload['stages']);

        $payload['completed_at'] = $payload['status'] === 'completed'
            ? ($project->completed_at ?? now())
            : null;

        $project->update($payload);

        $this->syncStages($project, $stagesInput);
        $this->syncMembers($project, $memberIds, $project->owner_id, $project->manager_id);
        $project->recalculateProgress();

        return redirect()
            ->route('projects.show', $project)
            ->with('success', 'The project has been updated.');
    }

    public function destroy(Project $project): RedirectResponse
    {
        if ($project->tasks()->exists()) {
            return redirect()
                ->route('projects.show', $project)
                ->with('error', 'You cannot delete a project that has tasks.');
        }

        $project->delete();

        return redirect()
            ->route('projects.index')
            ->with('success', 'The project has been deleted.');
    }

    public function updateTaskStage(Request $request, Project $project, Task $task): RedirectResponse|JsonResponse
    {
        $this->authorize('manageTasks', $project);

        if ($task->project_id !== $project->id) {
            abort(404);
        }

        $validated = $request->validate([
            'project_stage_id' => ['nullable', 'exists:project_stages,id'],
        ]);

        $stage = null;
        if (! empty($validated['project_stage_id'])) {
            $stage = ProjectStage::query()
                ->where('project_id', $project->id)
                ->findOrFail((int) $validated['project_stage_id']);
        }

        $sortOrderQuery = Task::query()->where('project_id', $project->id);
        if ($stage) {
            $sortOrderQuery->where('project_stage_id', $stage->id);
        } else {
            $sortOrderQuery->whereNull('project_stage_id');
        }

        $nextSortOrder = ((int) $sortOrderQuery->max('sort_order')) + 1;

        $status = $task->status;
        if ($stage?->is_done) {
            $status = 'done';
        } elseif ($task->status === 'done') {
            $status = 'in_progress';
        }

        $task->update([
            'project_stage_id' => $stage?->id,
            'status' => $status,
            'sort_order' => $nextSortOrder,
            'completed_at' => $status === 'done' ? ($task->completed_at ?? now()) : null,
        ]);

        $task->refresh()->load(['assignee', 'creator']);
        $project->recalculateProgress();
        $this->broadcastToOthers(new ProjectTaskStageChanged($task));

        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Project task stage updated.',
                'task' => [
                    'id' => $task->id,
                    'title' => $task->title,
                    'project_id' => $task->project_id,
                    'project_stage_id' => $task->project_stage_id,
                    'status' => $task->status,
                    'priority' => $task->priority,
                    'assignee_name' => $task->assignee?->name,
                    'due_at' => $task->due_at?->toIso8601String(),
                    'tracked_hours' => (float) $task->tracked_hours,
                    'estimated_hours' => (float) $task->estimated_hours,
                    'url' => route('tasks.show', $task),
                    'edit_url' => route('tasks.show', $task),
                ],
            ]);
        }

        return back()->with('success', 'Project task stage updated.');
    }

    public function updateStatus(Request $request, Project $project): RedirectResponse|JsonResponse
    {
        $this->authorize('update', $project);

        $validated = $request->validate([
            'status' => ['required', Rule::in(array_keys(self::STATUS_LABELS))],
        ]);

        $status = (string) $validated['status'];

        $project->update([
            'status' => $status,
            'completed_at' => $status === 'completed'
                ? ($project->completed_at ?? now())
                : null,
        ]);

        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Project status updated.',
                'project' => [
                    'id' => $project->id,
                    'status' => $project->status,
                    'status_label' => __((string) (self::STATUS_LABELS[$project->status] ?? $project->status)),
                    'completed_at' => $project->completed_at?->toIso8601String(),
                ],
            ]);
        }

        return back()->with('success', 'Project status updated.');
    }

    public function storeStage(Request $request, Project $project): RedirectResponse|JsonResponse
    {
        $this->authorize('manageTasks', $project);

        $validated = $request->validate([
            'name' => ['required', 'string', 'max:80'],
            'color' => ['nullable', 'string', 'regex:/^#[0-9A-Fa-f]{6}$/'],
        ]);

        $name = trim((string) $validated['name']);
        $sortOrder = ((int) $project->stages()->max('sort_order')) + 1;
        $isDone = $this->detectDoneStage($name);

        $stage = $project->stages()->create([
            'name' => $name,
            'code' => $this->makeUniqueStageCode($project, $name, $sortOrder + 1),
            'sort_order' => $sortOrder,
            'color' => $this->resolveStageColor(
                $validated['color'] ?? null,
                null,
                $isDone,
                $sortOrder
            ),
            'is_done' => $isDone,
        ]);

        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Project stage created.',
                'stage' => [
                    'id' => $stage->id,
                    'name' => $stage->name,
                    'sort_order' => $stage->sort_order,
                    'color' => $stage->color,
                    'is_done' => (bool) $stage->is_done,
                ],
            ], 201);
        }

        return back()->with('success', 'Project stage created.');
    }

    public function destroyStage(Request $request, Project $project, ProjectStage $stage): RedirectResponse|JsonResponse
    {
        $this->authorize('manageTasks', $project);

        if ($stage->project_id !== $project->id) {
            abort(404);
        }

        $movedTaskIds = [];

        DB::transaction(function () use ($project, $stage, &$movedTaskIds): void {
            $nextSortOrder = ((int) Task::query()
                ->where('project_id', $project->id)
                ->whereNull('project_stage_id')
                ->max('sort_order')) + 1;

            $tasks = Task::query()
                ->where('project_id', $project->id)
                ->where('project_stage_id', $stage->id)
                ->orderBy('sort_order')
                ->get(['id']);

            foreach ($tasks as $task) {
                $task->update([
                    'project_stage_id' => null,
                    'sort_order' => $nextSortOrder,
                ]);

                $movedTaskIds[] = (int) $task->id;
                $nextSortOrder++;
            }

            $stage->delete();

            $project->stages()
                ->orderBy('sort_order')
                ->get()
                ->values()
                ->each(function (ProjectStage $item, int $index): void {
                    if ((int) $item->sort_order !== $index) {
                        $item->update(['sort_order' => $index]);
                    }
                });
        });

        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Project stage deleted.',
                'deleted_stage_id' => (int) $stage->id,
                'moved_task_ids' => $movedTaskIds,
            ]);
        }

        return back()->with('success', 'Project stage deleted.');
    }

    public function updateStages(Request $request, Project $project): RedirectResponse|JsonResponse
    {
        $this->authorize('manageTasks', $project);

        $validated = $request->validate([
            'stages' => ['required', 'array', 'min:1'],
            'stages.*.id' => ['required', 'integer'],
            'stages.*.color' => ['nullable', 'string', 'regex:/^#[0-9A-Fa-f]{6}$/'],
        ]);

        $stages = $project->stages()->orderBy('sort_order')->get()->keyBy('id');
        $sortOrder = 0;

        foreach ($validated['stages'] as $stagePayload) {
            $stageId = (int) ($stagePayload['id'] ?? 0);
            $stage = $stages->get($stageId);

            if (! $stage) {
                continue;
            }

            $stage->update([
                'sort_order' => $sortOrder,
                'color' => $this->resolveStageColor(
                    $stagePayload['color'] ?? null,
                    $stage->color,
                    (bool) $stage->is_done,
                    $sortOrder
                ),
            ]);

            $sortOrder++;
            $stages->forget($stageId);
        }

        foreach ($stages as $stage) {
            $stage->update([
                'sort_order' => $sortOrder,
            ]);
            $sortOrder++;
        }

        $updatedStages = $project->stages()
            ->orderBy('sort_order')
            ->get()
            ->map(fn (ProjectStage $stage): array => [
                'id' => $stage->id,
                'name' => $stage->name,
                'sort_order' => $stage->sort_order,
                'color' => $stage->color,
            ])
            ->values()
            ->all();

        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Project stages updated.',
                'stages' => $updatedStages,
            ]);
        }

        return back()->with('success', 'Project stages updated.');
    }

    /**
     * @return array<string, mixed>
     */
    private function validatedData(Request $request, ?Project $project = null): array
    {
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'code' => [
                'nullable',
                'string',
                'max:40',
                Rule::unique('projects', 'code')->ignore($project?->id),
            ],
            'description' => ['nullable', 'string'],
            'company_id' => ['nullable', 'exists:companies,id'],
            'deal_id' => ['nullable', 'exists:deals,id'],
            'owner_id' => ['nullable', 'exists:users,id'],
            'manager_id' => ['nullable', 'exists:users,id'],
            'status' => ['required', Rule::in(['planned', 'active', 'on_hold', 'completed', 'cancelled'])],
            'priority' => ['required', Rule::in(['low', 'medium', 'high', 'critical'])],
            'health' => ['required', Rule::in(['normal', 'warning', 'risk'])],
            'budget' => ['nullable', 'numeric', 'min:0'],
            'spent' => ['nullable', 'numeric', 'min:0'],
            'starts_at' => ['nullable', 'date'],
            'due_at' => ['nullable', 'date'],
            'visibility' => ['required', Rule::in(['team', 'private', 'public'])],
            'notes' => ['nullable', 'string'],
            'stages' => ['nullable', 'string'],
            'members' => ['nullable', 'array'],
            'members.*' => ['integer', 'exists:users,id'],
        ]);

        if (! empty($validated['starts_at']) && ! empty($validated['due_at']) && $validated['due_at'] < $validated['starts_at']) {
            throw ValidationException::withMessages([
                'due_at' => 'The project deadline cannot be earlier than the start date.',
            ]);
        }

        $validated['code'] = $this->makeUniqueCode($validated['code'] ?? null, (string) $validated['name'], $project?->id);
        $validated['budget'] = (float) ($validated['budget'] ?? 0);
        $validated['spent'] = (float) ($validated['spent'] ?? 0);
        $validated['stages'] = trim((string) ($validated['stages'] ?? ''));
        $validated['members'] = array_values(array_unique($validated['members'] ?? []));

        return $validated;
    }

    private function makeUniqueCode(?string $inputCode, string $name, ?int $projectId = null): string
    {
        $base = Str::upper(Str::slug(trim((string) $inputCode), '_'));
        if ($base === '') {
            $base = Str::upper(Str::slug($name, '_'));
        }
        if ($base === '') {
            $base = 'PROJECT';
        }

        $base = Str::limit($base, 28, '');
        $code = $base;
        $counter = 1;

        while (Project::query()
            ->when($projectId, fn ($query) => $query->where('id', '!=', $projectId))
            ->where('code', $code)
            ->exists()) {
            $counter++;
            $code = Str::limit($base, 24, '').'_'.$counter;
        }

        return $code;
    }

    private function makeUniqueStageCode(Project $project, string $name, int $index): string
    {
        $base = Str::slug($name, '_');
        if ($base === '') {
            $base = 'stage';
        }

        $base = Str::lower(Str::limit($base, 36, ''));
        $suffix = max(1, $index);
        $code = "{$base}_{$suffix}";

        while ($project->stages()->where('code', $code)->exists()) {
            $suffix++;
            $code = "{$base}_{$suffix}";
        }

        return $code;
    }

    /**
     * @return array<string, array{label:string, header_class:string, surface_style:string}>
     */
    private function projectStatusOptions(): array
    {
        return [
            'planned' => [
                'label' => __('Planned'),
                'header_class' => 'border-slate-300 text-slate-700',
                'surface_style' => 'border-top-color: #94A3B8; background-image: linear-gradient(to bottom, rgba(148, 163, 184, 0.16), #FFFFFF 42%);',
            ],
            'active' => [
                'label' => __('Active'),
                'header_class' => 'border-sky-300 text-sky-700',
                'surface_style' => 'border-top-color: #0EA5E9; background-image: linear-gradient(to bottom, rgba(14, 165, 233, 0.16), #FFFFFF 42%);',
            ],
            'on_hold' => [
                'label' => __('On hold'),
                'header_class' => 'border-amber-300 text-amber-700',
                'surface_style' => 'border-top-color: #F59E0B; background-image: linear-gradient(to bottom, rgba(245, 158, 11, 0.16), #FFFFFF 42%);',
            ],
            'completed' => [
                'label' => __('Completed'),
                'header_class' => 'border-emerald-300 text-emerald-700',
                'surface_style' => 'border-top-color: #10B981; background-image: linear-gradient(to bottom, rgba(16, 185, 129, 0.16), #FFFFFF 42%);',
            ],
            'cancelled' => [
                'label' => __('Cancelled'),
                'header_class' => 'border-rose-300 text-rose-700',
                'surface_style' => 'border-top-color: #F43F5E; background-image: linear-gradient(to bottom, rgba(244, 63, 94, 0.16), #FFFFFF 42%);',
            ],
        ];
    }

    private function detectDoneStage(string $name): bool
    {
        $normalized = Str::lower(trim($name));

        return str_contains($normalized, 'done')
            || str_contains($normalized, 'complete')
            || str_contains($normalized, 'ready')
            || str_contains($normalized, 'finish');
    }

    private function syncStages(Project $project, string $rawStages): void
    {
        $stageNames = collect(preg_split('/\r\n|\r|\n/', $rawStages ?: ''))
            ->map(fn ($line) => trim((string) $line))
            ->filter()
            ->unique(fn ($line) => Str::lower($line))
            ->values();

        if ($stageNames->isEmpty()) {
            $stageNames = collect(['Backlog', 'In progress', 'Checking', 'Done']);
        }

        $existing = $project->stages()->get()->keyBy(fn ($stage) => Str::lower($stage->name));
        $sortOrder = 0;
        $doneExists = false;

        foreach ($stageNames as $name) {
            $normalized = Str::lower($name);
            $isDone = $this->detectDoneStage($name);

            $doneExists = $doneExists || $isDone;
            $stage = $existing->pull($normalized);

            $payload = [
                'name' => $name,
                'code' => Str::slug($name, '_').'_'.($sortOrder + 1),
                'sort_order' => $sortOrder,
                'color' => $this->resolveStageColor(
                    null,
                    $stage?->color,
                    $isDone,
                    $sortOrder
                ),
                'is_done' => $isDone,
            ];

            if ($stage) {
                $stage->update($payload);
            } else {
                $project->stages()->create($payload);
            }

            $sortOrder++;
        }

        if (! $doneExists) {
            $project->stages()->create([
                'name' => 'Ready',
                'code' => 'done_'.($sortOrder + 1),
                'sort_order' => $sortOrder,
                'color' => '#10B981',
                'is_done' => true,
            ]);
        }

        foreach ($existing as $stage) {
            if (! $stage->tasks()->exists()) {
                $stage->delete();
            }
        }
    }

    private function resolveStageColor(
        ?string $requestedColor,
        ?string $existingColor,
        bool $isDone,
        int $sortOrder
    ): string {
        $normalizedRequested = $this->normalizeStageColor($requestedColor);
        if ($normalizedRequested !== null) {
            return $normalizedRequested;
        }

        $normalizedExisting = $this->normalizeStageColor($existingColor);
        if ($normalizedExisting !== null) {
            return $normalizedExisting;
        }

        if ($isDone) {
            return '#10B981';
        }

        return self::STAGE_COLOR_PALETTE[$sortOrder % count(self::STAGE_COLOR_PALETTE)];
    }

    private function normalizeStageColor(?string $color): ?string
    {
        if (! is_string($color)) {
            return null;
        }

        $color = trim($color);
        if (! preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
            return null;
        }

        return strtoupper($color);
    }

    /**
     * @param  array<int, int|string>  $memberIds
     */
    private function syncMembers(Project $project, array $memberIds, ?int $ownerId, ?int $managerId): void
    {
        $ids = collect($memberIds)
            ->map(fn ($id) => (int) $id)
            ->filter()
            ->values();

        if ($ownerId) {
            $ids->push($ownerId);
        }

        if ($managerId) {
            $ids->push($managerId);
        }

        $now = now();
        $syncData = [];

        foreach ($ids->unique() as $id) {
            $role = 'contributor';
            if ($managerId && $id === $managerId) {
                $role = 'manager';
            }
            if ($ownerId && $id === $ownerId) {
                $role = 'owner';
            }

            $syncData[$id] = [
                'role' => $role,
                'joined_at' => $now,
            ];
        }

        $project->members()->sync($syncData);
    }

    /**
     * @return array{companies: Collection<int, Company>, deals: Collection<int, Deal>, users: Collection<int, User>}
     */
    private function formData(): array
    {
        $companies = Company::query()->orderBy('name')->get();
        $deals = Deal::query()->latest()->limit(300)->get();
        $users = User::query()->orderBy('name')->get();

        return compact('companies', 'deals', 'users');
    }
}
