<?php
declare(strict_types=1);

namespace App\Controller;

use Cake\Http\Client;

class LocationsController extends AppController
{
    public function initialize(): void
    {
        parent::initialize();

        if ($this->components()->has('Authentication')) {
            $this->Authentication->allowUnauthenticated(['search']);
        }
    }

    public function search()
    {
        $this->request->allowMethod(['get']);

        $query = trim((string)$this->request->getQuery('q'));

        if ($query === '' || mb_strlen($query) < 2) {
            return $this->response
                ->withType('application/json')
                ->withStringBody(json_encode([]));
        }

        $normalizedQuery = preg_replace('/\s+/', ' ', $query);
        $normalizedQuery = trim((string)$normalizedQuery);
        $freeformQuery = $normalizedQuery;

        $http = new Client([
            'timeout' => 8,
            'headers' => [
                'User-Agent' => 'ImpeccableCourier/1.0 (CakePHP address autocomplete)',
                'Accept-Language' => 'en;q=0.9',
            ],
        ]);

        try {
            $response = $http->get('https://nominatim.openstreetmap.org/search', [
                'q' => $freeformQuery,
                'format' => 'jsonv2',
                'addressdetails' => 1,
                'limit' => 12,
                'dedupe' => 1,
            ]);

            $results = json_decode((string)$response->getStringBody(), true);
            if (!is_array($results)) {
                $results = [];
            }

            $formatted = [];

            foreach ($results as $item) {
                $displayName = trim((string)($item['display_name'] ?? ''));
                $address = is_array($item['address'] ?? null) ? $item['address'] : [];

                if ($displayName === '') {
                    continue;
                }

                $primary = $this->buildPrimary($address, $displayName);
                $secondary = $this->buildSecondary($address, $displayName);

                $score = $this->scoreResult($normalizedQuery, $displayName, $primary, $address, $item);

                if ($score < 15 && mb_strlen($normalizedQuery) >= 3) {
                    continue;
                }

                $formatted[] = [
                    'display_name' => $displayName,
                    'primary' => $primary,
                    'secondary' => $secondary,
                    'score' => $score,
                ];
            }

            usort($formatted, function ($a, $b) {
                return $b['score'] <=> $a['score'];
            });

            $formatted = array_slice($formatted, 0, 6);

            $formatted = array_map(function ($item) {
                unset($item['score']);
                return $item;
            }, $formatted);

            return $this->response
                ->withType('application/json')
                ->withStringBody(json_encode($formatted));
        } catch (\Throwable $e) {
            return $this->response
                ->withType('application/json')
                ->withStringBody(json_encode([]));
        }
    }

    private function buildPrimary(array $address, string $displayName): string
    {
        $houseNumber = trim((string)($address['house_number'] ?? ''));
        $road = trim((string)($address['road'] ?? ''));
        $suburb = trim((string)($address['suburb'] ?? $address['city_district'] ?? $address['village'] ?? $address['town'] ?? $address['city'] ?? ''));

        if ($houseNumber !== '' && $road !== '') {
            return trim($houseNumber . ' ' . $road);
        }

        if ($road !== '') {
            return $road;
        }

        if ($suburb !== '') {
            return $suburb;
        }

        $parts = array_map('trim', explode(',', $displayName));
        return $parts[0] ?? $displayName;
    }

    private function buildSecondary(array $address, string $displayName): string
    {
        $parts = [];

        foreach ([
            'suburb', 'city_district', 'village', 'town', 'city',
            'county', 'state', 'postcode', 'country'
        ] as $key) {
            $value = trim((string)($address[$key] ?? ''));
            if ($value !== '' && !in_array($value, $parts, true)) {
                $parts[] = $value;
            }
        }

        if (!empty($parts)) {
            return implode(', ', $parts);
        }

        $displayParts = array_map('trim', explode(',', $displayName));
        array_shift($displayParts);
        return implode(', ', $displayParts);
    }

    private function scoreResult(string $query, string $displayName, string $primary, array $address, array $item): int
    {
        $score = 0;

        $queryLower = mb_strtolower($query);
        $displayLower = mb_strtolower($displayName);
        $primaryLower = mb_strtolower($primary);

        if (str_starts_with($primaryLower, $queryLower)) {
            $score += 120;
        }

        if (str_starts_with($displayLower, $queryLower)) {
            $score += 100;
        }

        if (mb_strpos($primaryLower, $queryLower) !== false) {
            $score += 55;
        }

        if (mb_strpos($displayLower, $queryLower) !== false) {
            $score += 35;
        }

        $hasRoad = !empty($address['road']);
        $hasHouseNumber = !empty($address['house_number']);
        $hasPostcode = !empty($address['postcode']);

        if ($hasRoad) {
            $score += 35;
        }

        if ($hasHouseNumber) {
            $score += 40;
        }

        if ($hasPostcode) {
            $score += 10;
        }

        if (preg_match('/^\d+/', $query, $m)) {
            $leadingDigits = $m[0];

            if (preg_match('/^\D*(\d+)/', $primary, $pm) && ($pm[1] ?? '') === $leadingDigits) {
                $score += 80;
            }

            if (preg_match('/^\D*(\d+)/', $displayName, $dm) && ($dm[1] ?? '') === $leadingDigits) {
                $score += 60;
            }

            if ($hasHouseNumber && (string)$address['house_number'] === $leadingDigits) {
                $score += 120;
            }
        }

        $type = (string)($item['type'] ?? '');
        $class = (string)($item['class'] ?? '');

        if (in_array($type, ['administrative', 'city', 'town', 'village', 'hamlet', 'suburb'], true)) {
            $score -= 45;
        }

        if ($class === 'boundary') {
            $score -= 35;
        }

        return $score;
    }
}