<?php

declare(strict_types=1);

namespace Maestro\Core\Modules\Offers\Http\Controllers;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Maestro\Core\Http\Controllers\Controller;
use Maestro\Core\Models\MaestroModule;
use Maestro\Core\Models\Organization;
use Maestro\Core\Modules\HourRegistration\Models\HourProject;
use Maestro\Core\Modules\Offers\Enums\OfferDiscountType;
use Maestro\Core\Modules\Offers\Enums\OfferStatus;
use Maestro\Core\Modules\Offers\Http\Requests\DuplicateOfferFromTemplateRequest;
use Maestro\Core\Modules\Offers\Http\Requests\SendOfferRequest;
use Maestro\Core\Modules\Offers\Http\Requests\StoreOfferRequest;
use Maestro\Core\Modules\Offers\Http\Requests\UpdateOfferRequest;
use Maestro\Core\Modules\Offers\Http\Requests\UpdateVatPresetsRequest;
use Maestro\Core\Modules\Offers\Http\Resources\OfferListResource;
use Maestro\Core\Modules\Offers\Http\Resources\OfferResource;
use Maestro\Core\Modules\Offers\Mail\OfferSentMailable;
use Maestro\Core\Modules\Offers\Models\MaestroOfferSettings;
use Maestro\Core\Modules\Offers\Models\Offer;
use Maestro\Core\Modules\Offers\Services\OfferDocumentBuilder;
use Maestro\Core\Modules\Offers\Services\OfferNumberService;
use Maestro\Core\Modules\Offers\Services\OfferPdfExporter;
use Maestro\Core\Modules\Offers\Services\OfferStructureSync;
use Maestro\Core\Modules\Offers\Services\OfferTotalsService;
use Symfony\Component\HttpFoundation\Response;

final class OfferController extends Controller
{
    public function metaEditorContext(Request $request): JsonResponse
    {
        abort_unless($request->user()?->can('offers.view'), 403);

        $settings = MaestroOfferSettings::instance();

        return response()->json([
            'vat_rate_presets' => $settings->vat_rate_presets ?? [21.0, 9.0, 0.0],
        ]);
    }

    public function patchVatPresets(UpdateVatPresetsRequest $request): JsonResponse
    {
        abort_unless($request->user()?->can('offers.catalog.manage'), 403);

        $settings = MaestroOfferSettings::instance();
        $settings->vat_rate_presets = array_values(array_map(static fn ($v) => round((float) $v, 4), $request->input('vat_rate_presets')));
        $settings->save();

        return response()->json([
            'data' => [
                'vat_rate_presets' => $settings->vat_rate_presets,
            ],
        ]);
    }

    public function index(Request $request): JsonResponse
    {
        abort_unless($request->user()?->can('offers.view'), 403);

        $query = Offer::query()->with('customer');

        if ($request->boolean('templates_only')) {
            $query->where('is_template', true);
        } else {
            $query->where('is_template', false);
        }

        $search = trim((string) $request->input('search', ''));
        if ($search !== '') {
            $like = '%'.addcslashes($search, '%_\\').'%';
            $query->where(function (Builder $q) use ($like): void {
                $q->where('offer_number', 'like', $like)->orWhere('subject', 'like', $like);
            });
        }

        $sortBy = (string) $request->input('sort_by', 'updated_at');
        $sortDir = strtolower((string) $request->input('sort_dir', 'desc')) === 'asc' ? 'asc' : 'desc';

        $allowedSort = ['updated_at', 'offer_number', 'subject', 'status', 'valid_until'];
        if (! in_array($sortBy, $allowedSort, true)) {
            $sortBy = 'updated_at';
        }

        $query->orderBy($sortBy, $sortDir);

        $perPage = min(max(request()->integer('per_page', 15), 1), 100);

        return OfferListResource::collection($query->paginate($perPage))->response();
    }

    public function store(StoreOfferRequest $request): JsonResponse
    {
        abort_unless($request->user()?->can('offers.create'), 403);

        $validated = $request->validated();

        $isTemplate = filter_var(($validated['is_template'] ?? false), FILTER_VALIDATE_BOOL);

        $offerNumber = $isTemplate ? OfferNumberService::allocateTemplateNumber() : OfferNumberService::allocateCommercialNumber();

        $status = OfferStatus::tryFrom((string) ($validated['status'] ?? '')) ?? OfferStatus::Draft;

        $discountType = OfferDiscountType::tryFrom((string) ($validated['discount_type'] ?? 'none'))
            ?? OfferDiscountType::None;

        $offer = Offer::query()->create([
            'offer_number' => $offerNumber,
            'subject' => $validated['subject'],
            'customer_id' => $validated['customer_id'] ?? null,
            'contact_id' => $validated['contact_id'] ?? null,
            'intro_html' => $validated['intro_html'] ?? null,
            'valid_until' => $validated['valid_until'] ?? null,
            'discount_type' => $discountType,
            'discount_value' => (float) ($validated['discount_value'] ?? 0),
            'status' => $status,
            'vat_total' => 0,
            'is_template' => $isTemplate,
            'template_name' => ($validated['template_name'] ?? null) ?: ($isTemplate ? $validated['subject'] : null),
            'created_by' => $request->user()?->getKey(),
            'updated_by' => $request->user()?->getKey(),
        ]);

        $sectionsPayload = isset($validated['sections']) ? (array) $validated['sections'] : [];

        OfferStructureSync::replace($offer, $sectionsPayload);
        $offer->load(['rootSections.children.rows', 'rootSections.rows', 'customer', 'contactPerson']);

        return response()->json([
            'data' => new OfferResource($offer),
        ], 201);
    }

    private static function hydrateForRead(Offer $offer): Offer
    {
        $offer->syncExpiryIfNeeded();
        $offer->load(['rootSections.children.rows.catalogItem', 'rootSections.rows.catalogItem', 'customer', 'contactPerson']);

        return $offer;
    }

    public static function guardEditable(Offer $offer): void
    {
        if ($offer->is_template) {
            return;
        }

        if ($offer->status !== OfferStatus::Draft) {
            abort(422, 'Offer is locked for editing.');
        }
    }

    public static function guardDeletable(Offer $offer): void
    {
        if ($offer->is_template) {
            return;
        }

        if ($offer->status !== OfferStatus::Draft) {
            abort(422, 'Only draft offers can be deleted.');
        }
    }

    public function show(Request $request, Offer $offer): JsonResponse
    {
        abort_unless($request->user()?->can('offers.view'), 403);

        $offer = self::hydrateForRead($offer);

        return response()->json(['data' => new OfferResource($offer)]);
    }

    public function update(UpdateOfferRequest $request, Offer $offer): JsonResponse
    {
        abort_unless($request->user()?->can('offers.edit'), 403);
        self::guardEditable($offer);

        $validated = $request->validated();

        $assign = [
            'subject' => $validated['subject'] ?? $offer->subject,
            'intro_html' => array_key_exists('intro_html', $validated) ? $validated['intro_html'] : $offer->intro_html,
            'customer_id' => array_key_exists('customer_id', $validated) ? $validated['customer_id'] : $offer->customer_id,
            'contact_id' => array_key_exists('contact_id', $validated) ? $validated['contact_id'] : $offer->contact_id,
            'valid_until' => array_key_exists('valid_until', $validated) ? $validated['valid_until'] : $offer->valid_until,
            'discount_value' => array_key_exists('discount_value', $validated) ? (float) $validated['discount_value'] : (float) $offer->discount_value,
            'template_name' => array_key_exists('template_name', $validated) ? $validated['template_name'] : $offer->template_name,
        ];

        if (array_key_exists('is_template', $validated)) {
            $offer->is_template = (bool) $validated['is_template'];
        }

        if (array_key_exists('discount_type', $validated)) {
            $dt = OfferDiscountType::tryFrom((string) $validated['discount_type']) ?? OfferDiscountType::None;
            $offer->discount_type = $dt;
        }

        /** @phpstan-ignore-next-line */
        $offer->fill($assign);

        /** @phpstan-ignore-next-line */
        $offer->updated_by = $request->user()?->getKey();
        $offer->save();

        if (isset($validated['sections'])) {
            OfferStructureSync::replace($offer, $validated['sections']);
        }

        OfferTotalsService::persistOfferVat($offer->fresh(['rootSections.children.rows', 'rootSections.rows']));
        $offer->refresh(['rootSections.children.rows', 'rootSections.rows', 'customer', 'contactPerson']);

        return response()->json(['data' => new OfferResource($offer)]);
    }

    public function destroy(Request $request, Offer $offer): JsonResponse
    {
        if ($offer->is_template) {
            abort_unless($request->user()?->can('offers.templates.manage'), 403);
        } else {
            abort_unless($request->user()?->can('offers.delete'), 403);
            self::guardDeletable($offer);
        }

        $offer->delete();

        return response()->json(['message' => 'Deleted.']);
    }

    public function pdf(Request $request, Offer $offer): Response
    {
        abort_unless($request->user()?->can('offers.view'), 403);

        $offer = self::hydrateForRead($offer);

        return OfferPdfExporter::downloadResponse($offer);
    }

    public function sendOffer(SendOfferRequest $request, Offer $offer): JsonResponse
    {
        abort_unless($request->user()?->can('offers.send'), 403);
        abort_if($offer->is_template, 422, 'Templates cannot be sent.');
        self::guardEditable($offer);
        abort_unless($offer->status === OfferStatus::Draft, 422, 'Offer has already been sent.');

        $offer->public_uuid ??= Str::uuid()->toString();
        $offer->status = OfferStatus::Sent;
        $offer->sent_at = now();
        /** @phpstan-ignore-next-line */
        $offer->updated_by = $request->user()?->getKey();
        $offer->save();

        $offer->refresh(['rootSections.children.rows', 'rootSections.rows', 'customer', 'contactPerson']);
        OfferTotalsService::persistOfferVat($offer);

        $doc = OfferDocumentBuilder::build($offer);
        $organization = Organization::instance();
        abort_if($organization === null, 422, 'Organization profile is missing. Fill Settings › Organization.');

        $channels = array_values(array_unique((array) $request->input('channels', [])));

        $publicUrl = url('/offers/public/'.$offer->public_uuid);

        $payloadOut = [
            'public_url' => null,
            'email_sent' => false,
            'authenticated_pdf_hint' => url('/api/offers/'.$offer->id.'/pdf'),
        ];

        if (in_array('link', $channels, true)) {
            $payloadOut['public_url'] = $publicUrl;
        }

        if (in_array('email', $channels, true)) {
            $emailCfg = $request->input('email', []);
            $to = isset($emailCfg['to']) ? trim((string) $emailCfg['to']) : '';
            abort_if($to === '', 422, 'Email recipient required when emailing the offer.');
            Mail::to($to)->send(new OfferSentMailable(
                $organization,
                $doc,
                $publicUrl,
                isset($emailCfg['subject']) ? (string) $emailCfg['subject'] : null,
            ));
            $payloadOut['email_sent'] = true;
            $payloadOut['public_url'] ??= $publicUrl;
        }

        return response()->json(['data' => $payloadOut]);
    }

    public function duplicateFromTemplate(DuplicateOfferFromTemplateRequest $request): JsonResponse
    {
        abort_unless($request->user()?->can('offers.create'), 403);

        $validated = $request->validated();
        /** @phpstan-ignore-next-line */
        $tpl = Offer::query()->whereKey((int) $validated['template_id'])->where('is_template', true)->firstOrFail();

        $discountType = $tpl->discount_type instanceof OfferDiscountType
            ? $tpl->discount_type
            : OfferDiscountType::tryFrom((string) $tpl->discount_type) ?? OfferDiscountType::None;

        /** @phpstan-ignore-next-line */
        $offer = Offer::query()->create([
            'offer_number' => OfferNumberService::allocateCommercialNumber(),
            'subject' => (string) ($validated['subject'] ?? $tpl->subject),
            'intro_html' => $tpl->intro_html,
            'valid_until' => null,
            'customer_id' => null,
            'contact_id' => null,
            'discount_type' => $discountType,
            'discount_value' => $tpl->discount_value,
            'status' => OfferStatus::Draft,
            'vat_total' => 0,
            'is_template' => false,
            'template_name' => null,
            /** @phpstan-ignore-next-line */
            'created_by' => $request->user()?->getKey(),
            /** @phpstan-ignore-next-line */
            'updated_by' => $request->user()?->getKey(),
        ]);

        OfferStructureSync::copyFromOffer($tpl, $offer);
        OfferTotalsService::persistOfferVat($offer->fresh(['rootSections.children.rows', 'rootSections.rows']));
        /** @phpstan-ignore-next-line */
        $offer->refresh(['rootSections.children.rows', 'rootSections.rows', 'customer', 'contactPerson']);

        return response()->json([
            'data' => new OfferResource($offer),
        ], 201);
    }

    public function convertToProject(Request $request, Offer $offer): JsonResponse
    {
        abort_unless($request->user()?->can('offers.convert.project'), 403);
        abort_unless($offer->status === OfferStatus::Won, 422, 'Offer must be marked as won.');
        abort_if((int) $offer->hour_project_id > 0, 422, 'Offer already linked to a project.');

        abort_unless(config('core.modules.hours', true) === true, 422, 'Hour registration module disabled.');
        abort_unless(Schema::hasTable('hour_projects'), 422);

        /** @phpstan-ignore-next-line */
        $hoursRuntime = MaestroModule::query()->where('slug', 'hours')->where('is_enabled', true)->exists();
        abort_unless($hoursRuntime, 422, 'Hour registration runtime module is disabled.');

        abort_if($offer->customer_id === null, 422, 'Offer must reference a customer to create a project.');

        /** @phpstan-ignore-next-line */
        $offer->loadMissing('customer');

        $name = trim((string) $request->input('name', ''));
        if ($name === '') {
            $name = $offer->subject !== '' ? $offer->subject : 'Project '.$offer->offer_number;
        }

        $projectLeadId = $offer->customer?->project_manager_id ?? $request->user()?->getKey();
        abort_if($projectLeadId === null || (int) $projectLeadId < 1, 422, 'Project lead is required. Set a customer project manager or retry as an authenticated user.');

        /** @phpstan-ignore-next-line */
        $hourProject = HourProject::query()->create([
            'name' => $name,
            'customer_id' => $offer->customer_id,
            'project_lead_id' => (int) $projectLeadId,
            'is_active' => true,
        ]);

        /** @phpstan-ignore-next-line */
        $offer->hour_project_id = $hourProject->id;
        $offer->updated_by = $request->user()?->getKey();
        $offer->save();
        /** @phpstan-ignore-next-line */
        $offer->refresh(['hourProject']);

        /** @phpstan-ignore-next-line */
        return response()->json(['data' => new OfferResource(self::hydrateForRead($offer))]);
    }
}
