<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Development;
use App\Models\Lot;
use App\Services\PlanDigitizer\PlanDigitizerService;
use App\Services\PlanInventorySynchronizer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
use Throwable;

/**
 * Digitizes an uploaded plan PDF into a dynamic masterplan.
 *
 * Reading is vector first — AutoCAD SHX annotations, PDF vectors and the plan's
 * own layer colours — and the page is rasterized only to produce the technical
 * backdrop the lots are drawn over.
 */
class MasterplanDigitizerController extends Controller
{
    /** Commercial status as the plan words it, mapped onto the inventory vocabulary. */
    private const STATUS_MAP = [
        'vendido' => 'sold',
        'apartado' => 'held',
        'disponible' => 'available',
        'bloqueado' => 'blocked',
    ];

    public function __construct(
        private readonly PlanDigitizerService $digitizer,
        private readonly PlanInventorySynchronizer $synchronizer,
    ) {}

    /**
     * The studio opens on an empty workspace. A stored masterplan is only loaded
     * when an existing project is named, so a new plan never lands on top of the
     * previous one.
     */
    public function show(Request $request): Response
    {
        $slug = Str::slug((string) $request->query('project', ''));
        $development = $slug === ''
            ? null
            : Development::query()->where('slug', $slug)->with('lots')->first();

        return Inertia::render('studio/masterplan', [
            'development' => $development === null ? null : [
                'id' => $development->id,
                'name' => $development->name,
                'slug' => $development->slug,
                'location' => $development->location,
                'sourceName' => $development->source_name,
                'sourcePage' => $development->source_page,
            ],
            'masterplan' => $development === null || $development->plan_frame === null ? null : [
                'frame' => $development->plan_frame,
                'report' => $development->plan_report,
                'legend' => $development->plan_legend,
                'layers' => $development->plan_layers,
                'source' => $development->plan_source,
                'basePlanUrl' => $development->source_image_path
                    ? Storage::disk('public')->url($development->source_image_path)
                    : null,
                'lots' => $development->lots->map(fn ($lot) => $this->lotPayload($lot))->values(),
            ],
            'projectsUrl' => route('admin.projects'),
        ]);
    }

    /** Persists a status change or note written straight on the masterplan. */
    public function updateLot(Request $request, int $lot): JsonResponse
    {
        $validated = $request->validate([
            'status' => ['nullable', Rule::in(array_values(self::STATUS_MAP))],
            'note' => ['nullable', 'string', 'max:2000'],
            'polygon' => ['nullable', 'array', 'min:3', 'max:200'],
            'polygon.*' => ['required', 'array', 'size:2'],
            'polygon.*.0' => ['required', 'numeric'],
            'polygon.*.1' => ['required', 'numeric'],
        ], [
            'polygon.min' => 'Un lote necesita al menos tres vértices.',
        ]);

        $model = Lot::query()->findOrFail($lot);

        $changes = array_filter([
            'status' => $validated['status'] ?? null,
            'note' => $validated['note'] ?? null,
        ], static fn (mixed $value): bool => $value !== null);

        if (isset($changes['status'])) {
            $changes['status_locked'] = true;
        }

        if (isset($validated['polygon'])) {
            $polygon = $this->normalizePolygon($validated['polygon']);

            if ($this->polygonArea($polygon) < 1.0) {
                throw ValidationException::withMessages([
                    'polygon' => 'El polígono corregido no encierra superficie.',
                ]);
            }

            $changes['points'] = $polygon;
            $changes['detection_source'] = 'manual';
            $changes['detection_confidence'] = 100;
            $changes['needs_review'] = false;
            $changes['missing_from_plan_at'] = null;
            $changes['area'] = $this->areaInMeters($model, $polygon);
        }

        $model->update($changes);

        return response()->json(['lot' => $this->lotPayload($model->fresh())]);
    }

    /**
     * @param  list<array{0: int|float, 1: int|float}>  $polygon
     * @return list<array{0: float, 1: float}>
     */
    private function normalizePolygon(array $polygon): array
    {
        $points = [];

        foreach ($polygon as $point) {
            $points[] = [round((float) $point[0], 2), round((float) $point[1], 2)];
        }

        return $points;
    }

    /** @param list<array{0: float, 1: float}> $polygon */
    private function polygonArea(array $polygon): float
    {
        $area = 0.0;
        $count = count($polygon);

        for ($index = 0; $index < $count; $index++) {
            $next = ($index + 1) % $count;
            $area += $polygon[$index][0] * $polygon[$next][1] - $polygon[$next][0] * $polygon[$index][1];
        }

        return abs($area) / 2;
    }

    /**
     * The plan frame stores metres per pixel, so a corrected polygon yields a
     * real surface without asking the operator to measure anything.
     *
     * @param  list<array{0: float, 1: float}>  $polygon
     */
    private function areaInMeters(Lot $lot, array $polygon): float
    {
        $metersPerPixel = $lot->development?->plan_frame['meters_per_pixel'] ?? null;

        if (! is_numeric($metersPerPixel) || (float) $metersPerPixel <= 0.0) {
            return (float) $lot->area;
        }

        return round($this->polygonArea($polygon) * ((float) $metersPerPixel ** 2), 2);
    }

    /** @return array<string, mixed> */
    private function lotPayload(Lot $lot): array
    {
        return [
            'databaseId' => $lot->id,
            'id' => $lot->external_id,
            'lot' => 'L-'.$lot->lot_number,
            'lotNumber' => $lot->lot_number,
            'block' => $lot->block === null ? null : str_replace('MZ-', '', $lot->block),
            'blockLabel' => $lot->block,
            'area' => $lot->area,
            'areaRaw' => $lot->area_raw,
            'dimensions' => array_map(
                static fn (array $measurement): string => (string) ($measurement['text'] ?? ''),
                $lot->measurements ?? [],
            ),
            'status' => $lot->status,
            'statusEvidence' => $lot->status_evidence,
            'statusConfidence' => $lot->status_confidence,
            'polygon' => $lot->points,
            'labelPoint' => $lot->label_point,
            'confidence' => $lot->detection_confidence,
            'pass' => $lot->digitization_pass,
            'needsReview' => $lot->needs_review,
            'note' => $lot->note ?? '',
        ];
    }

    /** Reads the plan without rasterizing so the operator can confirm what was found. */
    public function inspect(Request $request): JsonResponse
    {
        ini_set('memory_limit', '512M');
        set_time_limit(120);

        return $this->withUploadedPdf($request, function (string $path, int $page): JsonResponse {
            return response()->json($this->digitizer->inspect($path, $page));
        });
    }

    /** Runs the full digitization and stores the resulting masterplan. */
    public function digitize(Request $request): JsonResponse
    {
        // A large plan holds hundreds of thousands of segments plus a full-page
        // raster, which needs more headroom than a typical request.
        ini_set('memory_limit', '768M');
        set_time_limit(300);

        return $this->withUploadedPdf($request, function (string $path, int $page, Request $request): JsonResponse {
            $sourceName = (string) $request->input('source_name', 'plano.pdf');
            $development = $this->targetProject($request, $sourceName);
            $result = $this->digitizer->digitize($path, $page, [
                'targetEdge' => (int) $request->input('target_edge', 3600),
            ]);

            $imagePath = $this->storeBasePlan($development, $result['base_plan']['data']);
            $sync = $this->persist($development, $result, $imagePath, [
                'name' => $sourceName,
                'page' => $page,
            ]);

            return response()->json([
                'frame' => $result['frame'],
                'report' => $result['report'],
                'legend' => $result['legend'],
                'layers' => $result['layers'],
                'source' => $result['source'],
                'basePlanUrl' => Storage::disk('public')->url($imagePath),
                // Reload so the client receives the stored ids it needs to edit lots.
                'lots' => $development->lots()->get()
                    ->map(fn (Lot $lot): array => $this->lotPayload($lot))
                    ->values(),
                'projectUrl' => route('admin.projects.show', $development),
                'sync' => $sync,
            ]);
        });
    }

    /** @param callable(string, int, Request): JsonResponse $handler */
    private function withUploadedPdf(Request $request, callable $handler): JsonResponse
    {
        $validated = $request->validate([
            'file' => ['required', 'file', 'mimes:pdf', 'max:61440'],
            'page' => ['nullable', 'integer', 'min:1', 'max:2000'],
            'development_id' => ['nullable', 'integer', 'exists:developments,id'],
            'source_name' => ['nullable', 'string', 'max:255'],
            'target_edge' => ['nullable', 'integer', 'min:1200', 'max:6000'],
        ], [
            'file.required' => 'No se recibió el PDF del plano.',
            'file.mimes' => 'El archivo debe ser un PDF vectorial del plano.',
            'file.max' => 'El PDF supera el límite de 60 MB.',
        ]);

        $workspace = storage_path('app/private/masterplan/'.Str::uuid());
        File::ensureDirectoryExists($workspace);

        try {
            $request->file('file')->move($workspace, 'source.pdf');

            return $handler($workspace.'/source.pdf', (int) ($validated['page'] ?? 1), $request);
        } catch (Throwable $exception) {
            report($exception);

            return response()->json([
                'message' => $exception->getMessage() !== ''
                    ? $exception->getMessage()
                    : 'No fue posible procesar el plano.',
            ], 422);
        } finally {
            File::deleteDirectory($workspace);
        }
    }

    /**
     * Digitizing without naming a project creates one: a plan is a project, so a
     * second upload never overwrites the first.
     */
    private function targetProject(Request $request, string $sourceName): Development
    {
        $id = $request->input('development_id');

        if ($id !== null && $id !== '') {
            return Development::query()->findOrFail((int) $id);
        }

        $name = Str::headline(pathinfo($sourceName, PATHINFO_FILENAME)) ?: 'Proyecto sin nombre';
        $slug = Str::slug($name) ?: 'proyecto';
        $candidate = $slug;
        $suffix = 2;

        while (Development::query()->where('slug', $candidate)->exists()) {
            $candidate = $slug.'-'.$suffix;
            $suffix++;
        }

        return Development::query()->create([
            'name' => Str::limit($name, 140, ''),
            'slug' => $candidate,
            'location' => 'Por definir',
            'headline' => 'Lotes con inventario verificado desde el plano.',
            'meters_per_grid' => 3,
        ]);
    }

    private function storeBasePlan(Development $development, string $dataUri): string
    {
        $payload = explode(',', $dataUri, 2)[1] ?? '';
        $binary = base64_decode($payload, true);

        if ($binary === false || $binary === '') {
            abort(422, 'El plano rasterizado quedó vacío.');
        }

        $path = "developments/{$development->id}/masterplan.jpg";
        Storage::disk('public')->put($path, $binary);

        return $path;
    }

    /**
     * @param  array<string, mixed>  $result
     * @param  array{name: string, page: int}  $source
     * @return array{created: int, updated: int, removed: list<string>, kept: list<string>, preserved_status: list<string>}
     */
    private function persist(Development $development, array $result, string $imagePath, array $source): array
    {
        $pixelsPerMeter = $result['frame']['pixels_per_meter'] ?? null;

        $development->update([
            'source_name' => $source['name'],
            'source_type' => 'pdf',
            'source_page' => $source['page'],
            'source_image_path' => $imagePath,
            'meters_per_grid' => $pixelsPerMeter !== null && $pixelsPerMeter > 0
                ? round(1 / $pixelsPerMeter, 6)
                : $development->meters_per_grid,
            'plan_frame' => $result['frame'],
            'plan_report' => $result['report'],
            'plan_legend' => $result['legend'],
            'plan_layers' => array_slice($result['layers'], 0, 40),
            'plan_source' => $result['source'],
        ]);

        return $this->synchronizer->sync($development, $result['lots']);
    }
}
