<?php
/**
 * Copyright © 2018 EaDesign by Eco Active S.R.L. All rights reserved.
 * See LICENSE for license details.
 */

namespace Eadesigndev\GLS\Helper;

use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Magento\Framework\File\Csv;
use Magento\Framework\Filesystem\DirectoryList;

/**
 * Helper class for GLS Postal Code Lookup from CSV file
 */
class PostalCodeLookup extends AbstractHelper
{
    const CSV_FILE_PATH = 'gls.csv';
    const MAJOR_CITIES = ['BUCURESTI', 'IASI', 'CLUJ-NAPOCA', 'CLUJ NAPOCA'];
    const CITIES_WITHOUT_COUNTY_CHECK = ['BUCURESTI'];
    const MIN_SIMILARITY_THRESHOLD = 90;

    protected $csv;
    protected $directoryList;
    protected $postalData = null;

    public function __construct(
        Context $context,
        Csv $csv,
        DirectoryList $directoryList
    ) {
        parent::__construct($context);
        $this->csv = $csv;
        $this->directoryList = $directoryList;
    }

    /**
     * Get postal code for given city and street
     *
     * CSV Structure:
     * Column 0: Country code
     * Column 1: Zone
     * Column 2: Zip code
     * Column 3: Sub Zip code
     * Column 4: City name
     * Column 5: Street
     * Column 6-17: Other fields
     *
     * @param string $city
     * @param string $street
     * @param string $county County abbreviation (e.g., "HD", "IS", "CJ")
     * @return string|null
     */
    public function getPostalCode($city, $street, $county = '')
    {
        $city = $this->normalizeString($city);
        $street = $this->normalizeString($street);
        $county = strtoupper(trim($county));

        // Check if it's a major city (București, Iași, Cluj)
        $isMajorCity = $this->isMajorCity($city);

        // Load CSV data
        $data = $this->loadCsvData();

        if (empty($data)) {
            return null;
        }

        $matches = [];

        foreach ($data as $row) {
            $csvCity = isset($row[4]) ? $this->normalizeString($row[4]) : '';
            $csvStreet = isset($row[5]) ? $this->normalizeString($row[5]) : '';
            $csvZipCode = isset($row[2]) ? trim($row[2]) : '';

            if (empty($csvZipCode)) {
                continue;
            }

            $cityMatch = $this->matchCity($city, $csvCity, $county);

            if (!$cityMatch) {
                continue;
            }

            if (!$isMajorCity) {
                return $csvZipCode;
            }

            $similarity = $this->calculateStreetSimilarity($street, $csvStreet);

            if ($similarity > 0) {
                $matches[] = [
                    'zip_code' => $csvZipCode,
                    'similarity' => $similarity,
                    'city' => $csvCity,
                    'street' => $csvStreet
                ];
            }
        }

        if (!empty($matches)) {
            usort($matches, function($a, $b) {
                return $b['similarity'] <=> $a['similarity'];
            });

            $bestMatch = $matches[0];

            if ($bestMatch['similarity'] >= self::MIN_SIMILARITY_THRESHOLD) {
                return $bestMatch['zip_code'];
            } else {
                return null;
            }
        }

        // For major cities, if no good match found, don't return a default postal code
        // This prevents returning incorrect postal codes for Bucharest addresses
        if ($isMajorCity) {
            return null;
        }

        foreach ($data as $row) {
            $csvCity = isset($row[4]) ? $this->normalizeString($row[4]) : '';
            $csvZipCode = isset($row[2]) ? trim($row[2]) : '';

            if ($this->matchCity($city, $csvCity, $county) && !empty($csvZipCode)) {
                return $csvZipCode;
            }
        }

        return null;
    }

    /**
     * Check if city is a major city
     *
     * @param string $city
     * @return bool
     */
    protected function isMajorCity($city)
    {
        $city = $this->normalizeString($city);

        foreach (self::MAJOR_CITIES as $majorCity) {
            if (strpos($city, $this->normalizeString($majorCity)) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Match city name with county abbreviation
     *
     * @param string $orderCity City from order
     * @param string $csvCity City from CSV (e.g., "Petrosani HD")
     * @param string $county County abbreviation (e.g., "HD")
     * @return bool
     */
    protected function matchCity($orderCity, $csvCity, $county = '')
    {
        $skipCountyCheck = false;
        foreach (self::CITIES_WITHOUT_COUNTY_CHECK as $city) {
            if (strpos($orderCity, $this->normalizeString($city)) !== false) {
                $skipCountyCheck = true;
                break;
            }
        }

        if ($skipCountyCheck) {
            if (strpos($csvCity, $orderCity) === 0) {
                return true;
            }
            if ($orderCity === $csvCity) {
                return true;
            }
            return false;
        }

        if (!empty($county)) {
            $expectedCity = $orderCity . ' ' . $county;
            if ($csvCity === $expectedCity) {
                return true;
            }

            $pattern = '/^' . preg_quote($orderCity, '/') . '\s+' . preg_quote($county, '/') . '$/i';
            if (preg_match($pattern, $csvCity)) {
                return true;
            }

            if (preg_match('/\s+[A-Z]{1,2}$/i', $csvCity)) {
                return false;
            }
        }

        if ($orderCity === $csvCity) {
            return true;
        }

        if (empty($county) && strpos($csvCity, $orderCity) === 0) {
            return true;
        }

        return false;
    }

    /**
     * Calculate street similarity between order street and CSV street
     *
     * @param string $orderStreet Street from order (e.g., "sos. Nicolina nr. 160V")
     * @param string $csvStreet Street from CSV (e.g., "sosea Nicolina nr. 58-92")
     * @return float Similarity score (0-100)
     */
    protected function calculateStreetSimilarity($orderStreet, $csvStreet)
    {
        // Extract main street name (remove prefixes and numbers)
        $orderStreetName = $this->extractStreetName($orderStreet);
        $csvStreetName = $this->extractStreetName($csvStreet);

        if (empty($orderStreetName) || empty($csvStreetName)) {
            return 0;
        }

        // Extract street number from order
        $orderNumber = $this->extractStreetNumber($orderStreet);
        $csvNumberRange = $this->extractStreetNumber($csvStreet);

        // Debug logging
        $this->_logger->debug("Comparing streets - Order: '{$orderStreetName}' (num: {$orderNumber}) vs CSV: '{$csvStreetName}' (range: {$csvNumberRange})");

        // Check if street names match exactly
        if ($orderStreetName === $csvStreetName) {
            // If we have a number in order and a range in CSV, check if number is in range
            if (!empty($orderNumber) && !empty($csvNumberRange)) {
                if ($this->isNumberInRange($orderNumber, $csvNumberRange)) {
                    return 100;
                }
                return 90;
            }
            return 100;
        }

        // NEW: Check for word-order variations (e.g., "ION INCULET" vs "INCULET ION")
        $orderWords = $this->extractStreetWords($orderStreetName);
        $csvWords = $this->extractStreetWords($csvStreetName);

        if ($this->wordsMatchIgnoringOrder($orderWords, $csvWords)) {
            if (!empty($orderNumber) && !empty($csvNumberRange)) {
                if ($this->isNumberInRange($orderNumber, $csvNumberRange)) {
                    return 98;
                }
                return 88;
            }
            return 98;
        }

        if (strpos($csvStreetName, $orderStreetName) !== false || strpos($orderStreetName, $csvStreetName) !== false) {
            if (!empty($orderNumber) && !empty($csvNumberRange) && $this->isNumberInRange($orderNumber, $csvNumberRange)) {
                return 95;
            }
            return 85;
        }

        // Calculate Levenshtein distance for fuzzy matching
        $distance = levenshtein($orderStreetName, $csvStreetName);
        $maxLength = max(strlen($orderStreetName), strlen($csvStreetName));

        if ($maxLength == 0) {
            return 0;
        }

        // Reject matches where difference is too large
        // For "CRANGASI" (8 chars) vs "CREANGA ION" (11 chars):
        // distance = 7, which is > 25% of max length (11) = 2.75
        $maxAllowedDistance = ceil($maxLength * 0.25); // Allow max 25% difference
        if ($distance > $maxAllowedDistance) {
            return 0; // Reject this match completely
        }

        $similarity = (1 - ($distance / $maxLength)) * 100;

        // Further reduce similarity if distance is still significant
        if ($distance > 2) {
            $similarity = $similarity * 0.6; // Heavy penalty for differences
        }

        return max(0, $similarity);
    }

    /**
     * Extract significant words from street name for order-independent comparison
     * Filters out common short words that don't help identify the street
     *
     * @param string $streetName Normalized street name
     * @return array Array of significant words
     */
    protected function extractStreetWords($streetName)
    {
        // Split into words
        $words = preg_split('/\s+/', trim($streetName), -1, PREG_SPLIT_NO_EMPTY);

        // Filter out very short words (1-2 chars) that are likely connectors
        // But keep them if they're the only words we have
        $significantWords = array_filter($words, function($word) {
            return strlen($word) > 2;
        });

        // If we filtered out everything, keep the original words
        if (empty($significantWords)) {
            $significantWords = $words;
        }

        // Sort for order-independent comparison
        sort($significantWords);

        return $significantWords;
    }

    /**
     * Check if two word arrays match regardless of order
     * Example: ["ION", "INCULET"] matches ["INCULET", "ION"]
     *
     * @param array $words1 First array of words
     * @param array $words2 Second array of words
     * @return bool True if words match (ignoring order)
     */
    protected function wordsMatchIgnoringOrder($words1, $words2)
    {
        // Must have same number of words
        if (count($words1) !== count($words2)) {
            return false;
        }

        // Both arrays are already sorted in extractStreetWords()
        // So we can compare them directly
        return $words1 === $words2;
    }

    /**
     * Extract street name from full address
     * Examples:
     * - "sos. Nicolina nr. 160V" -> "nicolina"
     * - "Strada Nicolina" -> "nicolina"
     * - "sosea Nicolina nr. 58-92" -> "nicolina"
     *
     * @param string $street
     * @return string
     */
    protected function extractStreetName($street)
    {
        // Remove common prefixes
        $prefixes = ['b-dul', 'strada', 'str\.', 'str', 'alee', 'sosea', 'sos\.', 'sos', 'bulevardul', 'bulevard', 'bdul', 'bld\.', 'bld', 'bd\.', 'bd', 'calea', 'cale'];
        $pattern = '/\b(' . implode('|', $prefixes) . ')\b\.?\s*/i';
        $street = preg_replace($pattern, '', $street);

        // Remove numbers and everything after (including "nr. 160V Bl.11 Sc.2 Ap.51")
        $street = preg_replace('/[,\s]+nr\.?\s*\d+.*$/i', '', $street); // Remove ", Nr.40 Bl.11..." and everything after
        $street = preg_replace('/\s+\d+-\d+\s*$/i', '', $street); // Remove number ranges like "58-92"
        $street = preg_replace('/\s+\d+\s*$/i', '', $street); // Remove trailing numbers

        // Clean up punctuation and extra spaces
        $street = preg_replace('/[,;]+/', '', $street); // Remove commas, semicolons
        $street = preg_replace('/\s+/', ' ', $street); // Normalize spaces

        return trim($street);
    }

    /**
     * Extract street number from address
     * Examples:
     * - "Calea Crangasi, Nr.40 Bl.11" -> "40"
     * - "Cale Crangasi nr. 36-40" -> "36-40"
     * - "Strada Creanga" -> ""
     *
     * @param string $street
     * @return string Street number or range (e.g., "40" or "36-40")
     */
    protected function extractStreetNumber($street)
    {
        // Match "nr. 40" or "Nr.40" or just "40" after street name
        if (preg_match('/\bnr\.?\s*(\d+(?:-\d+)?)/i', $street, $matches)) {
            return $matches[1];
        }

        // Match standalone number range like "36-40"
        if (preg_match('/\b(\d+-\d+)\b/', $street, $matches)) {
            return $matches[1];
        }

        return '';
    }

    /**
     * Check if a number is within a range
     * Examples:
     * - isNumberInRange("40", "36-40") -> true
     * - isNumberInRange("40", "50-60") -> false
     * - isNumberInRange("40", "40") -> true
     *
     * @param string $number Single number
     * @param string $range Single number or range (e.g., "40" or "36-40")
     * @return bool
     */
    protected function isNumberInRange($number, $range)
    {
        $number = (int)$number;

        // Check if range contains a dash (e.g., "36-40")
        if (strpos($range, '-') !== false) {
            list($min, $max) = explode('-', $range);
            $min = (int)trim($min);
            $max = (int)trim($max);
            return $number >= $min && $number <= $max;
        }

        // Single number comparison
        return $number === (int)$range;
    }

    /**
     * Normalize string for comparison (lowercase, remove diacritics)
     *
     * @param string $string
     * @return string
     */
    protected function normalizeString($string)
    {
        $string = mb_strtoupper($string, 'UTF-8');
        $string = trim($string);

        // Remove Romanian diacritics
        $diacritics = [
            'Ă' => 'A', 'Â' => 'A', 'Î' => 'I', 'Ș' => 'S', 'Ț' => 'T',
            'ă' => 'a', 'â' => 'a', 'î' => 'i', 'ș' => 's', 'ț' => 't'
        ];
        $string = str_replace(array_keys($diacritics), array_values($diacritics), $string);

        return $string;
    }

    /**
     * Load CSV data from root directory
     * CSV uses semicolon (;) as delimiter
     *
     * @return array
     */
    protected function loadCsvData()
    {
        if ($this->postalData !== null) {
            return $this->postalData;
        }

        try {
            $rootPath = $this->directoryList->getRoot();
            $csvFilePath = $rootPath . '/' . self::CSV_FILE_PATH;

            if (!file_exists($csvFilePath)) {
                $this->_logger->error('GLS CSV file not found: ' . $csvFilePath);
                return [];
            }

            // Set delimiter to semicolon for GLS CSV format
            $this->csv->setDelimiter(';');
            $this->postalData = $this->csv->getData($csvFilePath);

            // Remove header row if exists
            if (!empty($this->postalData) && isset($this->postalData[0][0])) {
                $firstRow = $this->postalData[0];
                // Check if first row is header (contains "Country code" or "Zip code" text)
                if (stripos($firstRow[0], 'country') !== false || stripos($firstRow[2], 'zip') !== false) {
                    array_shift($this->postalData);
                }
            }

            return $this->postalData;
        } catch (\Exception $e) {
            $this->_logger->error('Error loading GLS CSV: ' . $e->getMessage());
            return [];
        }
    }
}
