<?php

namespace App;

class NameMatchService
{
    private $REMOVE = [
        'mr', 'mrs', 'ms', 'miss',
        'dr', 'shri', 'smt',
        'sau', 'sou',
        'w/o', 's/o',
        'd/o', 'c/o',
        'sh', 'shree', 'sri'
    ];

    private $SUFFIXES = [
        'rao', 'bhai', 'devi', 'kumar', 'lal', 'kant',
        'prasad', 'ben', 'banu', 'bai', 'laxmi', 'begam', 'begum'
    ];

    // MAIN FUNCTION
    public function nameMatch($name1, $name2)
    {
        $t1 = $this->cleanName($name1);
        $t2 = $this->cleanName($name2);

        if (empty($t1) || empty($t2)) {
            return null;
        }

        // EXACT MATCH
        if ($t1 === $t2) {
            return 100;
        }

        // REORDERED MATCH
        if ($this->sameTokenSet($t1, $t2)) {
            return 98;
        }

        // COMPOUND NAME MATCH (e.g. JAMELA BEGAM vs JAMELABEGAM)
        $compact1 = implode('', $t1);
        $compact2 = implode('', $t2);

        if ($compact1 === $compact2) {
            return 100;
        }

        // TOKEN SCORE
        list($tokenScore, $matches) = $this->calculateNameScore($t1, $t2);

        if (empty($matches)) {
            return 0;
        }

        // WHOLE NAME SCORE
        $wholeScore = $this->wholeNameScore($t1, $t2);

        // MATCH COVERAGE
        $matchedCount = count($matches);
        $smallerCount = min(count($t1), count($t2));
        $largerCount = max(count($t1), count($t2));
        $effectiveMatches = min($matchedCount, $smallerCount);
        $smallCoverage = min($effectiveMatches / $smallerCount, 1);
        $largeCoverage = min($effectiveMatches / $largerCount, 1);

        // EXACT TOKEN OVERLAP
        $overlap = count(array_intersect($t1, $t2));

        // V5 - DIFFERENT GIVEN NAME PROTECTION
        // Example: ANAND NARAYANRAO KHADSE vs ARYA ANAND KHADSE
        // These share ANAND + KHADSE but are different persons.
        $givenSimilarity = $this->givenNameSimilarity($t1, $t2);

        if (count($t1) >= 2 && count($t2) >= 2 && $givenSimilarity < 82) {
            $exactCommon = count(array_intersect($t1, $t2));

            // Do not allow surname/secondary token overlap to create a
            // high match when given names differ.
            if ($exactCommon < $smallerCount || $wholeScore < 90) {
                $protectedScore = $wholeScore * 0.75;
                return round(min($protectedScore, 69.99), 2);
            }
        }

        // STRONG MULTI TOKEN MATCH
        if ($smallCoverage >= 1 && $matchedCount >= 2 && $tokenScore >= 88 && $largeCoverage >= 0.66) {
            return round(min(max($tokenScore, $wholeScore), 100), 2);
        }

        // MULTIPLE COMMON TOKENS
        if ($overlap >= 2 && $smallCoverage >= 0.75 && $tokenScore >= 88) {
            return round(min(max($tokenScore, $wholeScore), 100), 2);
        }

        // GENERAL FUZZY SCORE
        $final = ($tokenScore * 0.60) + ($wholeScore * 0.30) + ($largeCoverage * 100 * 0.10);

        // SINGLE TOKEN OVERLAP PROTECTION
        if ($overlap == 1 && $matchedCount <= 1 && count($t1) > 1 && count($t2) > 1) {
            $final = min($final, 60);
        }

        // LOW COVERAGE PROTECTION
        if ($smallCoverage < 0.50) {
            $final = min($final, 60);
        }

        // ONE TOKEN NAME PROTECTION
        if (count($t1) == 1 || count($t2) == 1) {
            if ($t1[0] != $t2[0] && $tokenScore < 93) {
                $final = min($final, 75);
            }
        }

        // FINAL SAFETY CAP
        return round(min($final, 100), 2);
    }

    // CLEAN NAME
    private function cleanName($name)
    {
        if (!$name) {
            return [];
        }

        $name = strtolower(trim($name));

        // Normalize separators
        $name = str_replace(['-', '.', '/'], ' ', $name);

        // Remove relationship words
        $name = preg_replace('/\b(w\s*o|s\s*o|d\s*o|c\s*o)\b/', ' ', $name);

        // Remove special characters
        $name = preg_replace('/[^a-z0-9\s&]/', ' ', $name);

        // Handle &
        $name = explode('&', $name)[0];

        // Normalize spaces
        $name = preg_replace('/\s+/', ' ', trim($name));

        $tokens = [];

        foreach (preg_split('/\s+/', $name) as $token) {
            if (!$token || in_array($token, $this->REMOVE)) {
                continue;
            }

            // Keep compound token intact (e.g. JAMELABEGAM, PRAKASHBHAI)
            $tokens[] = $this->normalizeToken($token);
        }

        return $this->splitCompound($tokens);
    }

    // NORMALIZE TOKEN
    private function normalizeToken($x)
    {
        $x = strtolower(trim($x));

        // Do NOT remove suffixes here — we need PRAKASHBHAI to remain
        // available for compound matching.
        $x = str_replace(' ', '', $x);

        return $x;
    }

    // SPLIT COMPOUND
    private function splitCompound($tokens)
    {
        // Keep original tokens. Compound parts are handled separately
        // by expandTokens().
        return array_values(array_unique($tokens));
    }

    // COMPOUND VARIANTS
    private function compoundVariants($token)
    {
        $variants = [$token];

        foreach ($this->sortedSuffixes() as $suffix) {
            if (substr($token, -strlen($suffix)) === $suffix && $token !== $suffix) {
                $root = substr($token, 0, -strlen($suffix));

                if (strlen($root) >= 3) {
                    $variants[] = $root;
                    $variants[] = $suffix;
                    break;
                }
            }
        }

        return $variants;
    }

    // EXPAND TOKENS
    private function expandTokens($tokens)
    {
        $expanded = [];

        foreach ($tokens as $token) {
            if (!in_array($token, $expanded)) {
                $expanded[] = $token;
            }

            foreach ($this->compoundVariants($token) as $variant) {
                if (!in_array($variant, $expanded)) {
                    $expanded[] = $variant;
                }
            }
        }

        return $expanded;
    }

    // SORT SUFFIXES
    private function sortedSuffixes()
    {
        $suffixes = $this->SUFFIXES;

        usort($suffixes, function ($a, $b) {
            return strlen($b) - strlen($a);
        });

        return $suffixes;
    }

    // TOKEN SIMILARITY
    private function tokenSimilarity($a, $b)
    {
        if (!$a || !$b) {
            return 0;
        }

        // Exact match
        if ($a === $b) {
            return 100;
        }

        // Initial
        if (strlen($a) == 1 && strlen($b) >= 3 && strpos($b, $a) === 0) {
            return 96;
        }

        if (strlen($b) == 1 && strlen($a) >= 3 && strpos($a, $b) === 0) {
            return 96;
        }

        // Same after removing spaces
        if (str_replace(' ', '', $a) === str_replace(' ', '', $b)) {
            return 98;
        }

        // Contained name
        if (strlen($a) >= 5 && strlen($b) >= 5) {
            $shorter = min(strlen($a), strlen($b));
            $longer = max(strlen($a), strlen($b));

            if ((strpos($a, $b) !== false || strpos($b, $a) !== false) && ($shorter / $longer) >= 0.75) {
                return 94;
            }
        }

        // similar_text typo matching
        similar_text($a, $b, $percent);

        if ($percent >= 93) {
            return 96;
        }

        if ($percent >= 88) {
            return 92;
        }

        if ($percent >= 82 && strlen($a) >= 5 && strlen($b) >= 5) {
            return 85;
        }

        return 0;
    }

    // BEST TOKEN MATCHES
    private function bestTokenMatches($tokens1, $tokens2)
    {
        $candidates = [];

        foreach ($tokens1 as $i => $a) {
            foreach ($tokens2 as $j => $b) {
                $score = $this->tokenSimilarity($a, $b);

                if ($score > 0) {
                    $candidates[] = [
                        'score' => $score,
                        'i' => $i,
                        'j' => $j,
                        'a' => $a,
                        'b' => $b
                    ];
                }
            }
        }

        // Highest score first
        usort($candidates, function ($a, $b) {
            return $b['score'] <=> $a['score'];
        });

        $matches = [];
        $usedI = [];
        $usedJ = [];

        foreach ($candidates as $item) {
            if (in_array($item['i'], $usedI)) {
                continue;
            }

            if (in_array($item['j'], $usedJ)) {
                continue;
            }

            $matches[] = $item;
            $usedI[] = $item['i'];
            $usedJ[] = $item['j'];
        }

        return $matches;
    }

    // CALCULATE TOKEN SCORE
    private function calculateNameScore($tokens1, $tokens2)
    {
        if (empty($tokens1) || empty($tokens2)) {
            return [0, []];
        }

        $matches = $this->bestTokenMatches($tokens1, $tokens2);

        // Try compound expansion if normal matching does not find enough evidence.
        if (count($matches) < 2 || $this->maxMatchScore($matches) < 85) {
            $expanded1 = $this->expandTokens($tokens1);
            $expanded2 = $this->expandTokens($tokens2);

            $compoundMatches = $this->bestTokenMatches($expanded1, $expanded2);

            if (count($compoundMatches) > count($matches)) {
                $matches = $compoundMatches;
            }
        }

        if (empty($matches)) {
            return [0, []];
        }

        // Average token score
        $scores = [];

        foreach ($matches as $match) {
            $scores[] = $match['score'];
        }

        $averageScore = array_sum($scores) / count($scores);

        // Coverage is calculated against ORIGINAL token counts.
        // This prevents compound expansion from artificially increasing the score.
        $smallerTokens = min(count($tokens1), count($tokens2));
        $largerTokens = max(count($tokens1), count($tokens2));
        $effectiveMatches = min(count($matches), $smallerTokens);
        $smallCoverage = min($effectiveMatches / $smallerTokens, 1);
        $largeCoverage = min($effectiveMatches / $largerTokens, 1);

        $score = ($averageScore * 0.65) + ($smallCoverage * 100 * 0.20) + ($largeCoverage * 100 * 0.15);

        return [round(min($score, 100), 2), $matches];
    }

    // MAX MATCH SCORE
    private function maxMatchScore($matches)
    {
        $max = 0;

        foreach ($matches as $match) {
            if ($match['score'] > $max) {
                $max = $match['score'];
            }
        }

        return $max;
    }

    // WHOLE NAME SCORE
    private function wholeNameScore($tokens1, $tokens2)
    {
        $name1 = implode(' ', $tokens1);
        $name2 = implode(' ', $tokens2);

        // Token-sort comparison
        $sorted1 = $tokens1;
        $sorted2 = $tokens2;

        sort($sorted1);
        sort($sorted2);

        $tokenSort = $this->similarity(implode(' ', $sorted1), implode(' ', $sorted2));

        // Token-set comparison
        $unique1 = array_values(array_unique($tokens1));
        $unique2 = array_values(array_unique($tokens2));

        sort($unique1);
        sort($unique2);

        $tokenSet = $this->similarity(implode(' ', $unique1), implode(' ', $unique2));

        // Compact comparison
        $compactRatio = $this->similarity(
            str_replace(' ', '', $name1),
            str_replace(' ', '', $name2)
        );

        return round(max($tokenSort, $tokenSet, $compactRatio), 2);
    }

    // SAME TOKEN SET
    private function sameTokenSet($tokens1, $tokens2)
    {
        $a = array_values(array_unique($tokens1));
        $b = array_values(array_unique($tokens2));

        sort($a);
        sort($b);

        return $a === $b;
    }

    // GIVEN NAME SIMILARITY
    private function givenNameSimilarity($tokens1, $tokens2)
    {
        if (empty($tokens1) || empty($tokens2)) {
            return 0;
        }

        return $this->tokenSimilarity($tokens1[0], $tokens2[0]);
    }

    // SIMILARITY
    private function similarity($a, $b)
    {
        if ($a === $b) {
            return 100;
        }

        if ($a === '' || $b === '') {
            return 0;
        }

        similar_text($a, $b, $percent);

        return round($percent, 2);
    }

    // MATCH RESULT WITH REASON
    public function nameMatchWithReason($name1, $name2)
    {
        $score = $this->nameMatch($name1, $name2);

        return [
            'score' => $score,
            'reason' => $this->getMatchReason($name1, $name2, $score)
        ];
    }

    // MATCH REASON
    private function getMatchReason($name1, $name2, $score)
    {
        $t1 = $this->cleanName($name1);
        $t2 = $this->cleanName($name2);

        if ($t1 === $t2) {
            return 'EXACT_MATCH';
        }

        if ($this->sameTokenSet($t1, $t2)) {
            return 'REORDERED_MATCH';
        }

        if (implode('', $t1) === implode('', $t2)) {
            return 'COMPOUND_NAME_MATCH';
        }

        if (count($t1) >= 2 && count($t2) >= 2 && $this->givenNameSimilarity($t1, $t2) < 82) {
            return 'DIFFERENT_GIVEN_NAME';
        }

        if ($score >= 90) {
            return 'FUZZY_MULTI_TOKEN_MATCH';
        }

        if ($score >= 70) {
            return 'PARTIAL_MATCH';
        }

        return 'LOW_CONFIDENCE';
    }


    public function calculateEbillNameMatch($applicationId, $responseData) {
        $resultArray = [];
        $application = ConsumerApplication::where('id', $applicationId)->first(['name', 'co_applicant_name']);
        $score = $this->nameMatch(strtolower($application->name), strtolower($responseData->consumer_name));
        $applicantNamePercentage = is_numeric($score) ? round($score, 2) : 0;
        $resultArray['applicant_match'] = round($applicantNamePercentage);
        if (!empty($application->co_applicant_name)) {
            $coAppscore = $this->nameMatch(strtolower($application->co_applicant_name), strtolower($responseData->consumer_name));
            $coApplicantNamePercentage = is_numeric($coAppscore) ? round($coAppscore, 2) : 0;
            $resultArray['co_applicant_match'] = round($coApplicantNamePercentage);
        }
        return $resultArray;
    }
}