<?php
if (!defined('ABSPATH')) exit;

class W5E_Address_Directory
{
    private const PLACE_RESULTS_LIMIT = 80;
    private const MUNICIPALITY_RESULTS_LIMIT = 40;
    private const ZIP_RESULTS_LIMIT = 40;
    private const STREET_RESULTS_LIMIT = 12;

    public static function init()
    {
        add_action('wp_enqueue_scripts', [__CLASS__, 'enqueue_assets']);
        add_filter('woocommerce_checkout_fields', [__CLASS__, 'tweak_checkout_fields'], 20);

        add_action('wp_ajax_w5e_fetch_place_suggestions', [__CLASS__, 'ajax_places']);
        add_action('wp_ajax_nopriv_w5e_fetch_place_suggestions', [__CLASS__, 'ajax_places']);
        add_action('wp_ajax_w5e_fetch_municipality_suggestions', [__CLASS__, 'ajax_municipalities']);
        add_action('wp_ajax_nopriv_w5e_fetch_municipality_suggestions', [__CLASS__, 'ajax_municipalities']);
        add_action('wp_ajax_w5e_fetch_address_suggestions', [__CLASS__, 'ajax_addresses']);
        add_action('wp_ajax_nopriv_w5e_fetch_address_suggestions', [__CLASS__, 'ajax_addresses']);
        add_action('wp_ajax_w5e_fetch_zip_suggestions', [__CLASS__, 'ajax_zips']);
        add_action('wp_ajax_nopriv_w5e_fetch_zip_suggestions', [__CLASS__, 'ajax_zips']);
    }

    private static function is_enabled(): bool
    {
        return get_option('w5e_use_address_directory', 'no') === 'yes';
    }

    public static function enqueue_assets(): void
    {
        if (!self::is_enabled()) {
            return;
        }

        if (!function_exists('is_checkout') || !is_checkout()) {
            return;
        }
        if (function_exists('is_wc_endpoint_url') && is_wc_endpoint_url('order-received')) {
            return;
        }

        wp_enqueue_script('jquery-ui-autocomplete');

        $js_ver  = file_exists(W5E_PATH . 'assets/checkout-address.js') ? filemtime(W5E_PATH . 'assets/checkout-address.js') : W5E_VER;
        $css_ver = file_exists(W5E_PATH . 'assets/checkout-address.css') ? filemtime(W5E_PATH . 'assets/checkout-address.css') : W5E_VER;

        wp_enqueue_script(
            'w5e-checkout-address',
            W5E_URL . 'assets/checkout-address.js',
            ['jquery', 'jquery-ui-autocomplete'],
            $js_ver,
            true
        );

        wp_localize_script('w5e-checkout-address', 'w5eAddressDirectory', [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'i18n'    => [
                'noResults'     => __('Nema rezultata za "%s".', 'woo-5e'),
                'pickCityFirst' => __('Prvo odaberite grad/opštinu.', 'woo-5e'),
                'pickMunicipalityFirst' => __('Prvo odaberite opštinu.', 'woo-5e'),
                'pickPlaceFirst' => __('Prvo odaberite mesto.', 'woo-5e'),
            ],
        ]);

        wp_enqueue_style(
            'w5e-checkout-address',
            W5E_URL . 'assets/checkout-address.css',
            [],
            $css_ver
        );
    }

    public static function tweak_checkout_fields(array $fields): array
    {
        if (!self::is_enabled()) {
            return $fields;
        }

        foreach (['billing', 'shipping'] as $section) {
            $postcode_key = $section . '_postcode';
            if (isset($fields[$section][$postcode_key])) {
                $attrs = $fields[$section][$postcode_key]['custom_attributes'] ?? [];
                if (is_array($attrs)) {
                    unset($attrs['readonly'], $attrs['tabindex']);
                    $fields[$section][$postcode_key]['custom_attributes'] = $attrs;
                }
            }

            $city_key = $section . '_city';
            $state_key = $section . '_state';
            $postcode_key = $section . '_postcode';
            $address1_key = $section . '_address_1';
            $address2_key = $section . '_address_2';
            if (isset($fields[$section][$city_key])) {
                $fields[$section][$city_key]['priority'] = 60;
                $fields[$section][$city_key]['label'] = __('Town', 'woo-5e');
                $fields[$section][$city_key]['placeholder'] = __('Town', 'woo-5e');
            }
            if (isset($fields[$section][$state_key])) {
                $fields[$section][$state_key]['type'] = 'text';
                $fields[$section][$state_key]['required'] = false;
                $fields[$section][$state_key]['priority'] = 65;
                $fields[$section][$state_key]['label'] = __('Municipality', 'woo-5e');
                $state_attrs = $fields[$section][$state_key]['custom_attributes'] ?? [];
                if (!is_array($state_attrs)) {
                    $state_attrs = [];
                }
                unset($state_attrs['readonly'], $state_attrs['disabled']);
                $state_attrs['tabindex'] = -1;
                $fields[$section][$state_key]['custom_attributes'] = $state_attrs;

                $state_classes = $fields[$section][$state_key]['class'] ?? [];
                if (!is_array($state_classes)) {
                    $state_classes = [];
                }
                if (!in_array('hidden', $state_classes, true)) {
                    $state_classes[] = 'hidden';
                }
                $fields[$section][$state_key]['class'] = array_values(array_unique($state_classes));

                $state_input_classes = $fields[$section][$state_key]['input_class'] ?? [];
                if (!is_array($state_input_classes)) {
                    $state_input_classes = [];
                }
                if (!in_array('hidden', $state_input_classes, true)) {
                    $state_input_classes[] = 'hidden';
                }
                $fields[$section][$state_key]['input_class'] = array_values(array_unique($state_input_classes));
            }
            if (isset($fields[$section][$postcode_key])) {
                $fields[$section][$postcode_key]['priority'] = 67;
                $fields[$section][$postcode_key]['label'] = __('Post Number', 'woo-5e');
                $fields[$section][$postcode_key]['placeholder'] = __('Post Number', 'woo-5e');
            }
            if (isset($fields[$section][$address1_key])) {
                $fields[$section][$address1_key]['priority'] = 70;
                $fields[$section][$address1_key]['label'] = __('Address', 'woo-5e');
                $fields[$section][$address1_key]['placeholder'] = __('Address', 'woo-5e');
            }
            if (isset($fields[$section][$address2_key])) {
                $fields[$section][$address2_key]['priority'] = 80;
            }
        }

        return $fields;
    }


    public static function ajax_places(): void
    {
        if (!self::is_enabled()) {
            wp_send_json([]);
        }

        if (!isset($_GET['term'])) {
            wp_send_json([]);
        }

        $term = sanitize_text_field(wp_unslash($_GET['term']));
        $municipality = isset($_GET['municipality']) ? sanitize_text_field(wp_unslash($_GET['municipality'])) : '';
        if ($term === '') {
            wp_send_json([]);
        }

        $places = self::load_places();
        if (empty($places)) {
            wp_send_json([]);
        }

        if ($municipality !== '') {
            $filtered_places = self::filter_places_by_municipality($places, $municipality);
            if (!empty($filtered_places)) {
                $places = $filtered_places;
            }
        }

        $matches = self::search_places($places, $term, self::PLACE_RESULTS_LIMIT);
        wp_send_json($matches);
    }

    public static function ajax_municipalities(): void
    {
        if (!self::is_enabled()) {
            wp_send_json([]);
        }

        if (!isset($_GET['term'])) {
            wp_send_json([]);
        }

        $term = sanitize_text_field(wp_unslash($_GET['term']));
        if ($term === '') {
            wp_send_json([]);
        }

        $places = self::load_places();
        if (empty($places)) {
            wp_send_json([]);
        }

        $matches = self::search_municipalities($places, $term, self::MUNICIPALITY_RESULTS_LIMIT);
        wp_send_json($matches);
    }

    public static function ajax_addresses(): void
    {
        if (!self::is_enabled()) {
            wp_send_json([]);
        }

        if (!isset($_GET['term']) || (!isset($_GET['zip'], $_GET['place']) && !isset($_GET['place_id']))) {
            wp_send_json([]);
        }

        $term  = sanitize_text_field(wp_unslash($_GET['term']));
        $zip   = isset($_GET['zip']) ? sanitize_text_field(wp_unslash($_GET['zip'])) : '';
        $place = isset($_GET['place']) ? sanitize_text_field(wp_unslash($_GET['place'])) : '';
        $place_id = isset($_GET['place_id']) ? sanitize_text_field(wp_unslash($_GET['place_id'])) : '';

        if ($place_id === '' && ($zip === '' || $place === '')) {
            wp_send_json([]);
        }

        $places = self::load_places();
        if (empty($places)) {
            wp_send_json([]);
        }

        if ($place_id === '') {
            $place_id = self::find_place_id($places, $place, $zip);
        }
        if (!$place_id) {
            wp_send_json([]);
        }

        $streets = self::get_streets_for_place($place_id);
        if (empty($streets)) {
            wp_send_json([]);
        }

        $results = self::search_streets($streets, $term, self::STREET_RESULTS_LIMIT);
        wp_send_json($results);
    }

    public static function ajax_zips(): void
    {
        if (!self::is_enabled()) {
            wp_send_json([]);
        }

        if (!isset($_GET['term'])) {
            wp_send_json([]);
        }

        $term = sanitize_text_field(wp_unslash($_GET['term']));
        if ($term === '') {
            wp_send_json([]);
        }

        $places = self::load_places();
        if (empty($places)) {
            wp_send_json([]);
        }

        $matches = self::search_zips($places, $term, self::ZIP_RESULTS_LIMIT);
        wp_send_json($matches);
    }

    private static function get_data_dir(): string
    {
        $json_dir = W5E_PATH . 'ts-json';
        if (is_dir($json_dir)) {
            return $json_dir;
        }

        $ts_dir = W5E_PATH . 'ts';
        if (is_dir($ts_dir)) {
            return $ts_dir;
        }

        $fallback_json = WP_CONTENT_DIR . '/plugins/woo-5e 2/ts-json';
        if (is_dir($fallback_json)) {
            return $fallback_json;
        }

        $fallback_ts = WP_CONTENT_DIR . '/plugins/woo-5e 2/ts';
        if (is_dir($fallback_ts)) {
            return $fallback_ts;
        }

        return $ts_dir;
    }

    private static function get_cache_dir(): string
    {
        $uploads = wp_upload_dir();
        $base = isset($uploads['basedir']) ? $uploads['basedir'] : '';
        if (!$base) {
            return W5E_PATH . 'cache';
        }

        return trailingslashit($base) . 'w5e-address-cache';
    }

    private static function load_places(): array
    {
        static $cached = null;
        if (is_array($cached)) {
            return $cached;
        }

        $cache_dir = self::get_cache_dir();
        $places_file = trailingslashit($cache_dir) . 'places.json';

        if (file_exists($places_file)) {
            $raw = file_get_contents($places_file);
            $data = json_decode($raw, true);
            if (is_array($data)) {
                $first = reset($data);
                if (is_array($first) && array_key_exists('place_id', $first)) {
                    $cached = $data;
                    return $cached;
                }
            }
        }

        $data_dir = self::get_data_dir();
        $places = [];

        $municipalities_json = $data_dir . '/municipalities.json';
        $places_json = $data_dir . '/places.json';

        if (file_exists($municipalities_json) && file_exists($places_json)) {
            $municipalities = self::read_municipalities_json($municipalities_json);
            $places = self::read_places_json($places_json, $municipalities);
        } else {
            $municipalities = self::read_municipalities_ts($data_dir . '/municipalities.ts');
            $places = self::read_places_ts($data_dir . '/places.ts', $municipalities);
        }

        if (!empty($places)) {
            wp_mkdir_p($cache_dir);
            file_put_contents($places_file, wp_json_encode($places, JSON_UNESCAPED_UNICODE));
        }

        $cached = $places;
        return $cached;
    }

    public static function resolve_place_from_city(string $city, string $postcode): array
    {
        $city = trim($city);
        $postcode = trim($postcode);
        if ($city === '' || $postcode === '') {
            return [
                'place_id' => '',
                'place'    => $city,
                'municipality' => '',
            ];
        }

        $places = self::load_places();
        if (empty($places)) {
            return [
                'place_id' => '',
                'place'    => $city,
                'municipality' => '',
            ];
        }

        $match = self::find_place_match($places, $city, $postcode);
        if (!empty($match)) {
            return $match;
        }

        if (strpos($city, '-') !== false) {
            $candidate = preg_replace('/^[^-]+-/', '', $city);
            $candidate = trim((string) $candidate);
            if ($candidate !== '' && $candidate !== $city) {
                $match = self::find_place_match($places, $candidate, $postcode);
                if (!empty($match)) {
                    return $match;
                }
            }
        }

        return [
            'place_id' => '',
            'place'    => $city,
            'municipality' => '',
        ];
    }

    private static function read_municipalities_json(string $file): array
    {
        if (!file_exists($file)) {
            return [];
        }

        $raw = file_get_contents($file);
        $data = json_decode($raw, true);
        if (!is_array($data)) {
            return [];
        }

        $map = [];
        foreach ($data as $row) {
            if (!is_array($row)) {
                continue;
            }
            $id = $row['MunicipalitiesId'] ?? '';
            $name = $row['MunicipalitiesName'] ?? '';
            if ($id !== '') {
                $map[(string) $id] = (string) $name;
            }
        }

        return $map;
    }

    private static function read_places_json(string $file, array $municipalities): array
    {
        if (!file_exists($file)) {
            return [];
        }

        $raw = file_get_contents($file);
        $data = json_decode($raw, true);
        if (!is_array($data)) {
            return [];
        }

        $places = [];
        foreach ($data as $row) {
            if (!is_array($row)) {
                continue;
            }
            $place_id = $row['PlaceId'] ?? '';
            if ($place_id === '') {
                continue;
            }
            $muni_id = $row['Municipalities'] ?? '';
            $municipality_name = '';
            if ($muni_id !== '' && isset($municipalities[(string) $muni_id])) {
                $municipality_name = $municipalities[(string) $muni_id];
            }

            $places[] = [
                'place_id'     => (string) $place_id,
                'place'        => (string) ($row['PlaceName'] ?? ''),
                'municipality' => (string) $municipality_name,
                'zip'          => (string) ($row['Zip'] ?? ''),
            ];
        }

        return $places;
    }

    private static function read_municipalities_ts(string $file): array
    {
        if (!file_exists($file)) {
            return [];
        }

        $map = [];
        $current = [];
        $handle = fopen($file, 'r');
        if (!$handle) {
            return [];
        }

        while (($line = fgets($handle)) !== false) {
            if (preg_match("/MunicipalitiesId:\s*'([^']+)'/u", $line, $m)) {
                $current['id'] = $m[1];
            }
            if (preg_match("/MunicipalitiesName:\s*'([^']*)'/u", $line, $m)) {
                $current['name'] = $m[1];
            }
            if (strpos($line, '},') !== false && isset($current['id'])) {
                $map[$current['id']] = $current['name'] ?? '';
                $current = [];
            }
        }

        fclose($handle);
        return $map;
    }

    private static function read_places_ts(string $file, array $municipalities): array
    {
        if (!file_exists($file)) {
            return [];
        }

        $places = [];
        $current = [];
        $handle = fopen($file, 'r');
        if (!$handle) {
            return [];
        }

        while (($line = fgets($handle)) !== false) {
            if (preg_match("/PlaceId:\s*'([^']+)'/u", $line, $m)) {
                $current['place_id'] = $m[1];
            }
            if (preg_match("/PlaceName:\s*'([^']*)'/u", $line, $m)) {
                $current['place'] = $m[1];
            }
            if (preg_match("/Municipalities:\s*'([^']*)'/u", $line, $m)) {
                $current['municipality_id'] = $m[1];
            }
            if (preg_match("/Zip:\s*'([^']*)'/u", $line, $m)) {
                $current['zip'] = $m[1];
            }

            if (strpos($line, '},') !== false && isset($current['place_id'])) {
                $municipality_name = '';
                if (!empty($current['municipality_id']) && isset($municipalities[$current['municipality_id']])) {
                    $municipality_name = $municipalities[$current['municipality_id']];
                }

                $places[] = [
                    'place_id'     => $current['place_id'],
                    'place'        => $current['place'] ?? '',
                    'municipality' => $municipality_name,
                    'zip'          => $current['zip'] ?? '',
                ];
                $current = [];
            }
        }

        fclose($handle);
        return $places;
    }

    private static function search_places(array $places, string $term, int $limit): array
    {
        $term = trim($term);
        if ($term === '') {
            return [];
        }

        $prefix = [];
        $contains = [];
        $seen = [];

        foreach ($places as $item) {
            $name = isset($item['place']) ? (string) $item['place'] : '';
            if ($name === '') {
                continue;
            }

             $municipality = isset($item['municipality']) ? (string) $item['municipality'] : '';
             $display = self::format_place_with_prefix($places, $name, $municipality);
             $key = self::normalize_place_name($display . '|' . $municipality . '|' . ((string) ($item['zip'] ?? '')));
             if ($key === '' || isset($seen[$key])) {
                 continue;
             }

             $place_match = mb_stripos($name, $term, 0, 'UTF-8');
             $display_match = mb_stripos($display, $term, 0, 'UTF-8');
             $municipality_match = $municipality !== '' ? mb_stripos($municipality, $term, 0, 'UTF-8') : false;

             $is_prefix = ($place_match === 0 || $display_match === 0 || $municipality_match === 0);
             $is_contains = ($place_match !== false || $display_match !== false || $municipality_match !== false);

             if (!$is_contains) {
                 continue;
             }

             $seen[$key] = true;

            if ($is_prefix) {
                $prefix[] = $item;
            } else {
                $contains[] = $item;
            }
        }

        $merged = array_merge($prefix, $contains);
        $out = [];
        foreach ($merged as $item) {
            $municipality = $item['municipality'] ?? '';
            $out[] = [
                'place'        => $item['place'],
                'municipality' => $municipality,
                'zip'          => $item['zip'],
                'place_id'     => $item['place_id'] ?? '',
                'municipality_is_place' => self::municipality_is_place($places, (string) $municipality),
                'label'        => $item['place'],
                'value'        => $item['place'],
            ];
            if (count($out) >= $limit) {
                break;
            }
        }

        return $out;
    }

    private static function search_zips(array $places, string $term, int $limit): array
    {
        $term = trim($term);
        if ($term === '') {
            return [];
        }

        $prefix = [];
        $contains = [];
        $seen = [];

        foreach ($places as $item) {
            $zip = isset($item['zip']) ? (string) $item['zip'] : '';
            if ($zip === '') {
                continue;
            }
            if (isset($seen[$zip])) {
                continue;
            }

            if (strpos($zip, $term) === 0) {
                $prefix[] = $item;
                $seen[$zip] = true;
            } elseif (strpos($zip, $term) !== false) {
                $contains[] = $item;
                $seen[$zip] = true;
            }
        }

        $merged = array_merge($prefix, $contains);
        $out = [];
        foreach ($merged as $item) {
            $zip = isset($item['zip']) ? (string) $item['zip'] : '';
            if ($zip === '') {
                continue;
            }
            $place = isset($item['place']) ? (string) $item['place'] : '';
            $municipality = isset($item['municipality']) ? (string) $item['municipality'] : '';
            $place_display = self::format_place_with_prefix($places, $place, $municipality);
            $label = $zip;
            if ($place_display !== '') {
                $label = sprintf('%s — %s', $zip, $place_display);
                if ($municipality !== '' && strcasecmp($municipality, $place) !== 0) {
                    $label = sprintf('%s (%s)', $label, $municipality);
                }
            }
            $out[] = [
                'zip'          => $zip,
                'place'        => $place,
                'municipality' => $municipality,
                'municipality_is_place' => self::municipality_is_place($places, $municipality),
                'label'        => $label,
                'value'        => $zip,
                'place_id'     => isset($item['place_id']) ? (string) $item['place_id'] : '',
            ];
            if (count($out) >= $limit) {
                break;
            }
        }

        return $out;
    }

    private static function search_municipalities(array $places, string $term, int $limit): array
    {
        $term = trim($term);
        if ($term === '') {
            return [];
        }

        $all = [];
        $seen = [];
        foreach ($places as $item) {
            $municipality = isset($item['municipality']) ? trim((string) $item['municipality']) : '';
            if ($municipality === '') {
                continue;
            }
            $key = self::normalize_place_name($municipality);
            if ($key === '' || isset($seen[$key])) {
                continue;
            }
            $seen[$key] = true;
            $all[] = $municipality;
        }

        $place_counts = [];
        $place_groups = [];
        foreach ($places as $item) {
            $place = isset($item['place']) ? trim((string) $item['place']) : '';
            $key = self::normalize_place_name($place);
            if ($key === '') {
                continue;
            }

            if (!isset($place_counts[$key])) {
                $place_counts[$key] = [
                    'label' => $place,
                    'count' => 0,
                ];
            }

            $place_counts[$key]['count']++;

            $group = self::get_place_parent_city($place);
            $group_key = self::normalize_place_name($group);
            if ($group_key !== '') {
                if (!isset($place_groups[$group_key])) {
                    $place_groups[$group_key] = [
                        'label' => $group,
                        'count' => 0,
                    ];
                }

                $place_groups[$group_key]['count']++;
            }
        }

        foreach ($place_counts as $key => $meta) {
            if (isset($seen[$key])) {
                continue;
            }

            // Veći gradovi poput Beograda postoje kao mesto kroz više opština, ali nisu zasebna opština.
            if (($meta['count'] ?? 0) < 2) {
                continue;
            }

            $seen[$key] = true;
            $all[] = (string) $meta['label'];
        }

        foreach ($place_groups as $key => $meta) {
            if (isset($seen[$key])) {
                continue;
            }

            if (($meta['count'] ?? 0) < 2) {
                continue;
            }

            $seen[$key] = true;
            $all[] = (string) $meta['label'];
        }

        sort($all, SORT_LOCALE_STRING);

        $prefix = [];
        $contains = [];
        foreach ($all as $municipality) {
            if (mb_stripos($municipality, $term, 0, 'UTF-8') === 0) {
                $prefix[] = $municipality;
            } elseif (mb_stripos($municipality, $term, 0, 'UTF-8') !== false) {
                $contains[] = $municipality;
            }
        }

        $out = [];
        foreach (array_merge($prefix, $contains) as $municipality) {
            $out[] = [
                'municipality' => $municipality,
                'label' => $municipality,
                'value' => $municipality,
            ];
            if (count($out) >= $limit) {
                break;
            }
        }

        return $out;
    }

    private static function filter_places_by_municipality(array $places, string $municipality): array
    {
        $needle = self::normalize_place_name($municipality);
        if ($needle === '') {
            return $places;
        }

        $base_needle = self::normalize_place_name(self::get_base_city($municipality));
        $filtered = [];

        foreach ($places as $item) {
            $item_place = isset($item['place']) ? (string) $item['place'] : '';
            $item_place_key = self::normalize_place_name($item_place);
            $item_parent_city_key = self::normalize_place_name(self::get_place_parent_city($item_place));
            $item_municipality = isset($item['municipality']) ? (string) $item['municipality'] : '';
            $item_key = self::normalize_place_name($item_municipality);
            if ($item_key === '' && $item_place_key === '' && $item_parent_city_key === '') {
                continue;
            }

            $item_base = self::normalize_place_name(self::get_base_city($item_municipality));
            $is_exact = ($item_key === $needle);
            $is_base_match = ($base_needle !== '' && ($item_base === $base_needle || $item_key === $base_needle));
            $is_partial = (mb_strpos($item_key, $needle, 0, 'UTF-8') !== false || mb_strpos($needle, $item_key, 0, 'UTF-8') !== false);
            $is_place_match = ($item_place_key === $needle);
            $is_parent_city_match = ($item_parent_city_key === $needle);

            if (!($is_exact || $is_base_match || $is_partial || $is_place_match || $is_parent_city_match)) {
                continue;
            }

            $filtered[] = $item;
        }

        return $filtered;
    }

    private static function normalize_place_name(string $value): string
    {
        $value = trim($value);
        if ($value === '') {
            return '';
        }
        return mb_strtoupper($value, 'UTF-8');
    }

    private static function get_place_name_set(array $places): array
    {
        static $cached = null;
        if (is_array($cached)) {
            return $cached;
        }
        $set = [];
        foreach ($places as $item) {
            if (!isset($item['place'])) {
                continue;
            }
            $key = self::normalize_place_name((string) $item['place']);
            if ($key !== '') {
                $set[$key] = true;
            }
        }
        $cached = $set;
        return $cached;
    }

    private static function municipality_is_place(array $places, string $municipality): bool
    {
        $key = self::normalize_place_name($municipality);
        if ($key === '') {
            return false;
        }
        $set = self::get_place_name_set($places);
        return isset($set[$key]);
    }

    private static function get_base_city(string $municipality): string
    {
        $municipality = trim($municipality);
        if ($municipality === '') {
            return '';
        }
        if (strpos($municipality, '-') !== false) {
            $parts = array_map('trim', explode('-', $municipality));
            $parts = array_values(array_filter($parts, 'strlen'));
            if (!empty($parts)) {
                return $parts[count($parts) - 1];
            }
        }
        return $municipality;
    }

    private static function get_place_parent_city(string $place): string
    {
        $place = trim($place);
        if ($place === '' || strpos($place, '-') === false) {
            return '';
        }

        $parts = array_map('trim', explode('-', $place));
        $parts = array_values(array_filter($parts, 'strlen'));
        if (count($parts) < 2) {
            return '';
        }

        return $parts[0];
    }

    private static function format_place_with_prefix(array $places, string $place, string $municipality): string
    {
        $base_city = self::get_base_city($municipality);
        if ($place === '' || $base_city === '') {
            return $place;
        }
        $placeKey = self::normalize_place_name($place);
        $baseKey = self::normalize_place_name($base_city);
        if ($placeKey === '' || $baseKey === '') {
            return $place;
        }
        if (strpos($placeKey, $baseKey) === 0) {
            return $place;
        }
        return $base_city . '-' . $place;
    }

    private static function find_place_id(array $places, string $place, string $zip): string
    {
        $match = self::find_place_match($places, $place, $zip);

        return isset($match['place_id']) ? (string) $match['place_id'] : '';
    }

    private static function find_place_match(array $places, string $place, string $zip): array
    {
        $try_find = static function (string $candidate) use ($places, $zip): string {
            foreach ($places as $item) {
                if (!isset($item['place_id'], $item['place'], $item['zip'])) {
                    continue;
                }
                if ((string) $item['zip'] !== (string) $zip) {
                    continue;
                }
                if (mb_strtoupper((string) $item['place'], 'UTF-8') === mb_strtoupper($candidate, 'UTF-8')) {
                    return (string) $item['place_id'];
                }
            }
            return '';
        };

        $place = trim($place);
        if ($place === '' || trim($zip) === '') {
            return [];
        }

        $id = $try_find($place);
        if ($id !== '') {
            foreach ($places as $item) {
                if (
                    isset($item['place_id'], $item['place'], $item['zip']) &&
                    (string) $item['place_id'] === $id &&
                    (string) $item['zip'] === (string) $zip
                ) {
                    return [
                        'place_id' => (string) $item['place_id'],
                        'place' => (string) $item['place'],
                        'municipality' => (string) ($item['municipality'] ?? ''),
                    ];
                }
            }
        }

        // Saved checkout values may contain a base-city prefix (e.g. "BEOGRAD-ZEMUN").
        if (strpos($place, '-') !== false) {
            $candidate = preg_replace('/^[^-]+-/', '', $place);
            $candidate = trim((string) $candidate);
            if ($candidate !== '' && $candidate !== $place) {
                return self::find_place_match($places, $candidate, $zip);
            }
        }

        return [];
    }

    private static function get_streets_for_place(string $place_id): array
    {
        $cache_dir = self::get_cache_dir();
        $streets_dir = trailingslashit($cache_dir) . 'streets';
        $file = trailingslashit($streets_dir) . $place_id . '.json';

        if (file_exists($file)) {
            $raw = file_get_contents($file);
            $data = json_decode($raw, true);
            if (is_array($data)) {
                return $data;
            }
        }

        $data_dir = self::get_data_dir();
        $streets_json = $data_dir . '/streets.json';
        if (file_exists($streets_json)) {
            return self::build_streets_for_place_json($streets_json, $place_id, $streets_dir);
        }

        $streets_ts = $data_dir . '/streets.ts';
        return self::build_streets_for_place_ts($streets_ts, $place_id, $streets_dir);
    }

    private static function build_streets_for_place_json(string $file, string $place_id, string $streets_dir): array
    {
        if (!file_exists($file)) {
            return [];
        }

        $raw = file_get_contents($file);
        $data = json_decode($raw, true);
        if (!is_array($data)) {
            return [];
        }

        $list = [];
        foreach ($data as $row) {
            if (!is_array($row)) {
                continue;
            }
            $row_place = $row['PlaceId'] ?? '';
            if ((string) $row_place !== (string) $place_id) {
                continue;
            }
            $name = $row['StreetName'] ?? '';
            if ($name !== '') {
                $list[] = $name;
            }
        }

        $list = array_values(array_unique($list));
        sort($list, SORT_LOCALE_STRING);

        if (!empty($list)) {
            wp_mkdir_p($streets_dir);
            $out_file = trailingslashit($streets_dir) . $place_id . '.json';
            file_put_contents($out_file, wp_json_encode($list, JSON_UNESCAPED_UNICODE));
        }

        return $list;
    }

    private static function build_streets_for_place_ts(string $file, string $place_id, string $streets_dir): array
    {
        if (!file_exists($file)) {
            return [];
        }

        $list = [];
        $current = [];
        $handle = fopen($file, 'r');
        if (!$handle) {
            return [];
        }

        while (($line = fgets($handle)) !== false) {
            if (preg_match("/StreetName:\s*'([^']*)'/u", $line, $m)) {
                $current['name'] = $m[1];
            }
            if (preg_match("/PlaceId:\s*'([^']+)'/u", $line, $m)) {
                $current['place_id'] = $m[1];
            }

            if (strpos($line, '},') !== false && isset($current['place_id'])) {
                if ((string) $current['place_id'] === (string) $place_id && !empty($current['name'])) {
                    $list[] = $current['name'];
                }
                $current = [];
            }
        }

        fclose($handle);

        $list = array_values(array_unique($list));
        sort($list, SORT_LOCALE_STRING);

        if (!empty($list)) {
            wp_mkdir_p($streets_dir);
            $out_file = trailingslashit($streets_dir) . $place_id . '.json';
            file_put_contents($out_file, wp_json_encode($list, JSON_UNESCAPED_UNICODE));
        }

        return $list;
    }

    private static function search_streets(array $streets, string $term, int $limit): array
    {
        $term = trim($term);
        if ($term === '') {
            return [];
        }

        $prefix = [];
        $contains = [];

        foreach ($streets as $name) {
            if (mb_stripos($name, $term, 0, 'UTF-8') === 0) {
                $prefix[] = $name;
            } elseif (mb_stripos($name, $term, 0, 'UTF-8') !== false) {
                $contains[] = $name;
            }
        }

        $merged = array_merge($prefix, $contains);
        $out = [];
        foreach ($merged as $name) {
            $out[] = [
                'value' => $name,
                'label' => $name,
            ];
            if (count($out) >= $limit) {
                break;
            }
        }

        return $out;
    }
}
