<?php

namespace App\Http\Controllers;

use App\Events\DealStageChanged;
use App\Models\Company;
use App\Models\Contact;
use App\Models\Deal;
use App\Models\DealStage;
use App\Models\Pipeline;
use App\Models\User;
use App\Support\CrmModuleManager;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;

class DealController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Deal::class, 'deal');
    }

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

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

        $preferredViewMode = (string) ($user->preferred_deal_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_deal_view !== $viewMode) {
                    $user->forceFill([
                        'preferred_deal_view' => $viewMode,
                    ])->saveQuietly();
                }
            } else {
                $viewMode = 'list';
            }
        } else {
            $viewMode = $preferredViewMode;
        }

        $pipelines = Pipeline::query()->with(['stages' => fn ($query) => $query->orderBy('sort_order')])->orderBy('sort_order')->get();

        $activePipeline = null;
        if ($pipelines->isNotEmpty()) {
            $activePipeline = $pipelines->firstWhere('id', (int) $request->input('pipeline_id'))
                ?? $pipelines->firstWhere('is_default', true)
                ?? $pipelines->first();
        }

        $search = trim((string) $request->input('q', ''));

        $dealsQuery = Deal::query()
            ->with(['pipeline', 'stage', 'company', 'contact', 'owner'])
            ->when($activePipeline, fn ($query) => $query->where('pipeline_id', $activePipeline->id))
            ->when($search !== '', function ($query) use ($search): void {
                $query->where(function ($sub) use ($search): void {
                    $sub->where('title', 'like', "%{$search}%")
                        ->orWhereHas('company', fn ($company) => $company->where('name', 'like', "%{$search}%"))
                        ->orWhereHas('contact', function ($contact) use ($search): void {
                            $contact->where('first_name', 'like', "%{$search}%")
                                ->orWhere('last_name', 'like', "%{$search}%");
                        });
                });
            });

        $deals = null;
        $boardStages = collect();
        $canUpdateActivePipeline = false;
        if ($viewMode === 'kanban' && $activePipeline) {
            $canUpdateActivePipeline = $user->can('update', $activePipeline);
            $groupedDeals = (clone $dealsQuery)
                ->where('status', 'open')
                ->orderByDesc('amount')
                ->get()
                ->groupBy('stage_id');

            $boardStages = $activePipeline->stages->map(function (DealStage $stage) use ($groupedDeals): array {
                /** @var Collection<int, Deal> $stageDeals */
                $stageDeals = $groupedDeals->get($stage->id, collect());

                return [
                    'stage' => $stage,
                    'deals' => $stageDeals,
                    'amount' => $stageDeals->sum('amount'),
                ];
            });
        } else {
            $deals = (clone $dealsQuery)
                ->latest()
                ->paginate(20)
                ->withQueryString();
        }

        return view('deals.index', compact(
            'pipelines',
            'activePipeline',
            'search',
            'deals',
            'boardStages',
            'canUpdateActivePipeline',
            'viewOptions',
            'viewMode'
        ));
    }

    public function create(Request $request): View|RedirectResponse
    {
        $formData = $this->formData();

        if ($formData['pipelines']->isEmpty()) {
            return redirect()
                ->route('pipelines.create')
                ->with('error', 'First create a sales funnel.');
        }

        $selectedPipeline = $formData['pipelines']->firstWhere('id', (int) $request->input('pipeline_id'))
            ?? $formData['pipelines']->firstWhere('is_default', true)
            ?? $formData['pipelines']->first();

        if (! $selectedPipeline || $selectedPipeline->stages->isEmpty()) {
            return redirect()
                ->route('pipelines.edit', $selectedPipeline ?? $formData['pipelines']->first())
                ->with('error', 'Add funnel stages before creating a deal.');
        }

        return view('deals.create', [
            ...$formData,
            'selectedPipeline' => $selectedPipeline,
        ]);
    }

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

        $deal = Deal::create($payload);

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

    public function show(Request $request, Deal $deal): View
    {
        $deal->load([
            'pipeline',
            'stage',
            'company',
            'contact',
            'owner',
            'tasks.assignee',
            'tasks.creator',
            'activities.user',
            'activities.company',
            'activities.contact',
        ]);

        if ($request->boolean('sidepanel') || $request->header('X-Sidepanel') === '1') {
            return view('sidepanel.deals.show', compact('deal'));
        }

        return view('deals.show', compact('deal'));
    }

    public function edit(Deal $deal): View
    {
        $formData = $this->formData();

        $selectedPipeline = $formData['pipelines']->firstWhere('id', $deal->pipeline_id)
            ?? $formData['pipelines']->first();

        return view('deals.edit', [
            ...$formData,
            'deal' => $deal,
            'selectedPipeline' => $selectedPipeline,
        ]);
    }

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

        $deal->update($payload);

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

    public function destroy(Deal $deal): RedirectResponse
    {
        $deal->delete();

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

    public function updateStage(Request $request, Deal $deal): RedirectResponse|\Illuminate\Http\JsonResponse
    {
        $this->authorize('update', $deal);

        $validated = $request->validate([
            'stage_id' => ['required', 'exists:deal_stages,id'],
            'status' => ['nullable', Rule::in(['open', 'won', 'lost'])],
        ]);

        $stage = DealStage::query()
            ->whereKey((int) $validated['stage_id'])
            ->where('pipeline_id', $deal->pipeline_id)
            ->firstOrFail();

        $status = $validated['status'] ?? $deal->status;

        if ($stage->is_won) {
            $status = 'won';
        } elseif ($stage->is_lost) {
            $status = 'lost';
        }

        $deal->update([
            'stage_id' => $stage->id,
            'status' => $status,
            'closed_at' => $status === 'open' ? null : ($deal->closed_at ?? now()),
        ]);

        $deal->refresh()->load(['stage', 'company', 'contact', 'owner']);

        $this->broadcastToOthers(new DealStageChanged($deal));

        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'The transaction stage has been updated.',
                'deal' => [
                    'id' => $deal->id,
                    'title' => $deal->title,
                    'pipeline_id' => $deal->pipeline_id,
                    'stage_id' => $deal->stage_id,
                    'stage_name' => $deal->stage?->name,
                    'stage_color' => $deal->stage?->color,
                    'status' => $deal->status,
                    'amount' => (float) $deal->amount,
                    'company_name' => $deal->company?->name,
                    'contact_name' => $deal->contact?->full_name,
                    'owner_name' => $deal->owner?->name,
                ],
            ]);
        }

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

    /**
     * @return array<string, mixed>
     */
    private function validatedData(Request $request, ?Deal $deal = null): array
    {
        $validated = $request->validate([
            'title' => ['required', 'string', 'max:255'],
            'pipeline_id' => ['required', 'exists:pipelines,id'],
            'stage_id' => ['required', 'exists:deal_stages,id'],
            'company_id' => ['nullable', 'exists:companies,id'],
            'contact_id' => ['nullable', 'exists:contacts,id'],
            'owner_id' => ['nullable', 'exists:users,id'],
            'amount' => ['required', 'numeric', 'min:0'],
            'currency' => ['required', 'string', 'size:3'],
            'priority' => ['required', Rule::in(['low', 'medium', 'high'])],
            'status' => ['required', Rule::in(['open', 'won', 'lost'])],
            'expected_close_at' => ['nullable', 'date'],
            'source' => ['nullable', 'string', 'max:255'],
            'lost_reason' => ['nullable', 'string'],
            'description' => ['nullable', 'string'],
        ]);

        $stage = DealStage::query()->findOrFail((int) $validated['stage_id']);

        if ($stage->pipeline_id !== (int) $validated['pipeline_id']) {
            throw ValidationException::withMessages([
                'stage_id' => 'The selected stage does not belong to the selected funnel.',
            ]);
        }

        $status = $validated['status'];

        if ($stage->is_won) {
            $status = 'won';
        } elseif ($stage->is_lost) {
            $status = 'lost';
        }

        if ($status === 'lost' && trim((string) ($validated['lost_reason'] ?? '')) === '') {
            throw ValidationException::withMessages([
                'lost_reason' => 'Indicate the reason for losing the transaction.',
            ]);
        }

        $validated['currency'] = strtoupper($validated['currency']);
        $validated['status'] = $status;
        $validated['closed_at'] = $status === 'open'
            ? null
            : ($deal && $deal->status === $status && $deal->closed_at ? $deal->closed_at : now());

        return $validated;
    }

    /**
     * @return array{companies: Collection<int, Company>, contacts: Collection<int, Contact>, owners: Collection<int, User>, pipelines: Collection<int, Pipeline>}
     */
    private function formData(): array
    {
        $companies = Company::query()->orderBy('name')->get();
        $contacts = Contact::query()->orderBy('first_name')->orderBy('last_name')->get();
        $owners = User::query()->orderBy('name')->get();
        $pipelines = Pipeline::query()->with(['stages' => fn ($query) => $query->orderBy('sort_order')])->orderBy('sort_order')->get();

        return compact('companies', 'contacts', 'owners', 'pipelines');
    }
}
