<?php

namespace PortalPeople\shippingcosts\services;

/**
 * PostcodeExpansionService
 *
 * Handles postcode expansion and matching logic.
 * Converts postcode ranges and shorthand notation into individual postcodes.
 */
class PostcodeExpansionService
{
    /**
     * @var array<string, array<int, string>>
     */
    private static array $postcodeLookup = [];

    private static bool $postcodeLookupLoaded = false;

    /**
     * Process and expand postcode ranges into individual postcodes
     *
     * Supports:
     * - Comma-separated ranges: "AB1-5, CD10-12"
     * - Postcode prefixes: "AB, CD" (expands to all postcodes starting with prefix)
     * - Individual postcodes: "AB12, CD34"
     *
     * @param string $codes Postcode string to process
     * @return array List of individual postcodes
     */
    public static function expandPostcodes($codes): array
    {
        $data = explode(',', trim($codes));

        if (count($data) > 1) {
            $out = [];
            foreach ($data as $postcode) {
                $expanded = self::expandSinglePostcode($postcode);
                $out = array_merge($out, $expanded);
            }
            return $out;
        } elseif (count($data) == 1) {
            return self::expandSinglePostcode($data[0]);
        }

        return [];
    }

    /**
     * Expand a single postcode entry
     *
     * @param string $postcode Postcode entry (range or single)
     * @return array List of expanded postcodes
     */
    private static function expandSinglePostcode($postcode): array
    {
        $postcode = trim($postcode);

        // Check if it's a range like "AB1-5"
        if (strpos($postcode, '-') !== false) {
            return self::expandRange($postcode);
        }

        // Check if it's a prefix (2 chars or less) that should be looked up in POSTCODES constant
        if (strlen($postcode) <= 2) {
            return self::expandPrefix($postcode);
        }

        // It's a single postcode
        return [$postcode];
    }

    /**
     * Expand a postcode range like "AB1-5" to ["AB1", "AB2", "AB3", "AB4", "AB5"]
     *
     * @param string $postcode Range postcode like "AB1-5"
     * @return array List of postcodes in range
     */
    private static function expandRange($postcode): array
    {
        preg_match('/^[A-Za-z]+/', trim($postcode), $postarr);
        preg_match('/[0-9]+/', trim($postcode), $start);
        preg_match('/[0-9]+$/', trim($postcode), $endno);

        if (!isset($postarr[0]) || !isset($start[0]) || !isset($endno[0])) {
            return [$postcode];
        }

        $post = $postarr[0];
        $starter = (int) $start[0];
        $end = (int) $endno[0];

        $out = [];
        while ($starter <= $end) {
            $out[] = $post . $starter;
            $starter++;
        }

        return $out;
    }

    /**
     * Expand a postcode prefix using the POSTCODES lookup table
     *
     * Examples:
     * - "A" → all postcodes starting with A
     * - "AB" → all AB postcodes from POSTCODES constant
     * - "AB1" → specific AB1 postcode if in lookup
     *
     * @param string $postcode Postcode prefix or partial postcode
     * @return array List of postcodes
     */
    private static function expandPrefix($postcode): array
    {
        $out = [];
        $postcodeLookup = self::getPostcodeLookup();

        // Try single letter
        preg_match('/^[A-Z]/', trim($postcode), $s);
        // Try to get just the numeric part if present
        preg_match('/[0-9]$/', trim($postcode), $n);

        if (!isset($s[0])) {
            return [$postcode];
        }

        $prefix = strtoupper($s[0]);

        // If there's no numeric part, look up full postcode in constant
        if (!isset($n[0])) {
            $upperPostcode = strtoupper(trim($postcode));
            if (isset($postcodeLookup[$upperPostcode])) {
                return $postcodeLookup[$upperPostcode];
            }
        } else {
            // Has numeric part, check if it's a specific entry in POSTCODES
            $upperPostcode = strtoupper(trim($postcode));
            if (isset($postcodeLookup[$prefix])) {
                if (
                    is_array($postcodeLookup[$prefix]) &&
                    in_array($upperPostcode, $postcodeLookup[$prefix])
                ) {
                    $out[] = $upperPostcode;
                }
            }
        }

        return !empty($out) ? $out : [$postcode];
    }

    /**
     * Load postcode lookup map from JSON files in src/data/postcodes.
     *
     * Each file should contain an object map:
     * {
     *   "BN": ["BN1", "BN2"]
     * }
     *
     * @return array<string, array<int, string>>
     */
    private static function getPostcodeLookup(): array
    {
        if (self::$postcodeLookupLoaded) {
            return self::$postcodeLookup;
        }

        $lookup = [];
        $postcodeDir = dirname(__DIR__) . '/data/postcodes';
        $files = glob($postcodeDir . '/*.json');

        if (is_array($files)) {
            foreach ($files as $file) {
                $content = file_get_contents($file);
                if ($content === false) {
                    continue;
                }

                $decoded = json_decode($content, true);
                if (!is_array($decoded)) {
                    continue;
                }

                foreach ($decoded as $prefix => $codes) {
                    $normalizedPrefix = strtoupper(trim((string) $prefix));
                    if ($normalizedPrefix === '') {
                        continue;
                    }

                    if (!isset($lookup[$normalizedPrefix])) {
                        $lookup[$normalizedPrefix] = [];
                    }

                    if (!is_array($codes)) {
                        continue;
                    }

                    foreach ($codes as $code) {
                        $normalizedCode = strtoupper(trim((string) $code));
                        if ($normalizedCode !== '') {
                            $lookup[$normalizedPrefix][] = $normalizedCode;
                        }
                    }
                }
            }
        }

        foreach ($lookup as $prefix => $codes) {
            $lookup[$prefix] = array_values(array_unique($codes));
        }

        self::$postcodeLookup = $lookup;
        self::$postcodeLookupLoaded = true;

        return self::$postcodeLookup;
    }
}
