<?php

namespace App\Domains\Purchase\Services;

/**
 * Measures likely DNI typing errors using the optimal-string-alignment
 * variant of the Damerau-Levenshtein distance.
 *
 * The returned value is the minimum number of single-character edits needed
 * to transform one DNI into the other. Supported edits are insertion,
 * deletion, substitution and transposition of two adjacent digits.
 */
class DniDistanceService
{
    /**
     * Calculate the edit distance between two normalized DNI strings.
     *
     * Each matrix cell [row][column] stores the minimum edits required to
     * transform the first $row digits of $left into the first $column digits
     * of $right. The bottom-right cell therefore contains the final distance.
     */
    public function distance(string $left, string $right): int
    {
        $left = $this->normalize($left);
        $right = $this->normalize($right);
        $leftLength = strlen($left);
        $rightLength = strlen($right);
        $matrix = [];

        // Transforming a prefix into an empty string requires deleting every digit.
        for ($row = 0; $row <= $leftLength; $row++) {
            $matrix[$row] = [$row];
        }

        // Transforming an empty string into a prefix requires inserting every digit.
        for ($column = 0; $column <= $rightLength; $column++) {
            $matrix[0][$column] = $column;
        }

        for ($row = 1; $row <= $leftLength; $row++) {
            for ($column = 1; $column <= $rightLength; $column++) {
                $substitutionCost = $left[$row - 1] === $right[$column - 1] ? 0 : 1;
                $deletionDistance = $matrix[$row - 1][$column] + 1;
                $insertionDistance = $matrix[$row][$column - 1] + 1;
                $substitutionDistance = $matrix[$row - 1][$column - 1] + $substitutionCost;

                // Keep the cheapest way to align the two prefixes at this position.
                $matrix[$row][$column] = min(
                    $deletionDistance,
                    $insertionDistance,
                    $substitutionDistance,
                );

                // Count two adjacent inverted digits as one edit instead of two substitutions.
                if (
                    $row > 1
                    && $column > 1
                    && $left[$row - 1] === $right[$column - 2]
                    && $left[$row - 2] === $right[$column - 1]
                ) {
                    $matrix[$row][$column] = min(
                        $matrix[$row][$column],
                        $matrix[$row - 2][$column - 2] + 1,
                    );
                }
            }
        }

        return $matrix[$leftLength][$rightLength];
    }

    /**
     * Keep only digits and left-pad seven-digit DNIs so comparisons preserve
     * the leading zero that is present when the DNI is extracted from a CUIT.
     */
    public function normalize(string $dni): string
    {
        $digits = preg_replace('/\D+/', '', $dni) ?? '';

        return str_pad($digits, 8, '0', STR_PAD_LEFT);
    }
}
