<?php

namespace App\Services;

use App\Models\Development;
use App\Models\Lot;
use Illuminate\Support\Facades\DB;

/**
 * Applies a fresh digitization onto a project's existing inventory.
 *
 * Re-reading a corrected plan must never cost the sales team its work, so the
 * plan owns geometry and measurements while the project keeps prices, notes and
 * any status a person set by hand. Lots that vanish from the new plan are only
 * removed when nothing commercial depends on them.
 */
class PlanInventorySynchronizer
{
    /**
     * @param  list<array<string, mixed>>  $lots  the digitizer's lot payloads
     * @return array{created: int, updated: int, removed: list<string>, kept: list<string>, preserved_status: list<string>}
     */
    public function sync(Development $development, array $lots): array
    {
        /** @var array<string, Lot> $existing */
        $existing = $development->lots()->get()->keyBy('external_id')->all();
        $created = 0;
        $updated = 0;
        $preservedStatus = [];
        $seen = [];

        DB::transaction(function () use ($development, $lots, $existing, &$created, &$updated, &$preservedStatus, &$seen): void {
            foreach ($lots as $lot) {
                $id = (string) $lot['id'];
                $seen[$id] = true;
                $current = $existing[$id] ?? null;

                if ($current === null) {
                    $development->lots()->create($this->attributes($lot) + [
                        'external_id' => $id,
                        'code' => $id,
                        'price' => 0,
                        'status' => $this->status($lot),
                        'orientation' => 'Por definir',
                        'financing' => 'Por configurar',
                        'note' => '',
                    ]);
                    $created++;

                    continue;
                }

                $changes = $this->attributes($lot);

                // A status the operator set by hand outranks the plan's hatching.
                if ($current->status_locked) {
                    $preservedStatus[] = $id;
                } else {
                    $changes['status'] = $this->status($lot);
                }

                $current->update($changes);
                $updated++;
            }
        });

        [$removed, $kept] = $this->retireMissing($development, $existing, $seen);

        return [
            'created' => $created,
            'updated' => $updated,
            'removed' => $removed,
            'kept' => $kept,
            'preserved_status' => $preservedStatus,
        ];
    }

    /**
     * @param  array<string, Lot>  $existing
     * @param  array<string, bool>  $seen
     * @return array{0: list<string>, 1: list<string>}
     */
    private function retireMissing(Development $development, array $existing, array $seen): array
    {
        $removed = [];
        $kept = [];

        foreach ($existing as $id => $lot) {
            if (isset($seen[$id])) {
                continue;
            }

            if ($this->isCommitted($lot)) {
                $lot->update([
                    'needs_review' => true,
                    'missing_from_plan_at' => now(),
                ]);
                $kept[] = $id;

                continue;
            }

            $lot->delete();
            $removed[] = $id;
        }

        return [$removed, $kept];
    }

    /** A lot carries commercial weight once it has a price, a status, a note or a prospect. */
    private function isCommitted(Lot $lot): bool
    {
        return $lot->price > 0
            || $lot->status !== 'available'
            || trim((string) $lot->note) !== ''
            || $lot->prospects()->exists();
    }

    /**
     * Everything the plan is authoritative about. Prices, notes, orientation and
     * financing are absent on purpose: those belong to the project.
     *
     * @param  array<string, mixed>  $lot
     * @return array<string, mixed>
     */
    private function attributes(array $lot): array
    {
        $values = array_map(floatval(...), $lot['dimensions']);
        sort($values);

        return [
            'block' => $lot['block_label'],
            'lot_number' => (string) ($lot['lot_number'] ?? $lot['lot']),
            'area' => $lot['area_value'] ?? 0,
            'area_raw' => $lot['area_raw'],
            'status_evidence' => $lot['status_evidence'],
            'status_confidence' => $lot['status_confidence'],
            'points' => $lot['polygon'],
            'label_point' => $lot['label_point'],
            'frontage' => $values === [] ? 0 : $values[0],
            'depth' => $values === [] ? 0 : $values[count($values) - 1],
            'measurements' => $this->measurements($lot['dimensions']),
            'detection_source' => 'automatic',
            'detection_confidence' => $lot['confidence'],
            'digitization_pass' => $lot['pass'],
            'needs_review' => $lot['needs_review'],
            'missing_from_plan_at' => null,
        ];
    }

    /** @param array<string, mixed> $lot */
    private function status(array $lot): string
    {
        return match ($lot['status']) {
            'vendido' => 'sold',
            'apartado' => 'held',
            'bloqueado' => 'blocked',
            default => 'available',
        };
    }

    /**
     * @param  list<string>  $dimensions
     * @return list<array{side: string, value: float, text: string}>
     */
    private function measurements(array $dimensions): array
    {
        $measurements = [];

        foreach ($dimensions as $index => $text) {
            $measurements[] = [
                'side' => 'Lado '.($index + 1),
                'value' => (float) $text,
                'text' => $text,
            ];
        }

        return $measurements;
    }
}
