<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Validation;

/**
 * IBAN validation with MOD-97 checksum and country-specific format checks.
 *
 * Supports all European IBAN formats with special focus on DE (Germany).
 *
 * Part of Issue #26.
 */
class IbanValidator {

    /**
     * Country-specific IBAN lengths.
     * Source: ISO 13616 / SWIFT IBAN Registry.
     *
     * @var array<string, int>
     */
    private const COUNTRY_LENGTHS = [
        'AL' => 28, 'AD' => 24, 'AT' => 20, 'AZ' => 28, 'BH' => 22,
        'BY' => 28, 'BE' => 16, 'BA' => 20, 'BR' => 29, 'BG' => 22,
        'CR' => 22, 'HR' => 21, 'CY' => 28, 'CZ' => 24, 'DK' => 18,
        'DO' => 28, 'TL' => 23, 'EE' => 20, 'FO' => 18, 'FI' => 18,
        'FR' => 27, 'GE' => 22, 'DE' => 22, 'GI' => 23, 'GR' => 27,
        'GL' => 18, 'GT' => 28, 'HU' => 28, 'IS' => 26, 'IQ' => 23,
        'IE' => 22, 'IL' => 23, 'IT' => 27, 'JO' => 30, 'KZ' => 20,
        'XK' => 20, 'KW' => 30, 'LV' => 21, 'LB' => 28, 'LI' => 21,
        'LT' => 20, 'LU' => 20, 'MK' => 19, 'MT' => 31, 'MR' => 27,
        'MU' => 30, 'MC' => 27, 'MD' => 24, 'ME' => 22, 'NL' => 18,
        'NO' => 15, 'PK' => 24, 'PS' => 29, 'PL' => 28, 'PT' => 25,
        'QA' => 29, 'RO' => 24, 'LC' => 32, 'SM' => 27, 'ST' => 25,
        'SA' => 24, 'RS' => 22, 'SC' => 31, 'SK' => 24, 'SI' => 19,
        'ES' => 24, 'SE' => 24, 'CH' => 21, 'TN' => 24, 'TR' => 26,
        'UA' => 29, 'AE' => 23, 'GB' => 22, 'VG' => 24,
    ];

    /**
     * Validate an IBAN string.
     *
     * @param string $iban The IBAN to validate
     * @return bool True if the IBAN is valid
     */
    public static function isValid(string $iban): bool {
        $cleaned = self::clean($iban);

        if (strlen($cleaned) < 15 || strlen($cleaned) > 34) {
            return false;
        }

        // Must start with 2 letters + 2 digits
        if (!preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/', $cleaned)) {
            return false;
        }

        // Check country-specific length
        $country = substr($cleaned, 0, 2);
        if (isset(self::COUNTRY_LENGTHS[$country])) {
            if (strlen($cleaned) !== self::COUNTRY_LENGTHS[$country]) {
                return false;
            }
        }

        // MOD-97 checksum
        return self::mod97Check($cleaned);
    }

    /**
     * Get a human-readable validation error for an invalid IBAN.
     *
     * @param string $iban The IBAN to check
     * @return string|null Error message in German, or null if valid
     */
    public static function getError(string $iban): ?string {
        $cleaned = self::clean($iban);

        if (strlen($cleaned) === 0) {
            return 'IBAN darf nicht leer sein.';
        }

        if (strlen($cleaned) < 15) {
            return 'IBAN ist zu kurz (mindestens 15 Zeichen).';
        }

        if (strlen($cleaned) > 34) {
            return 'IBAN ist zu lang (maximal 34 Zeichen).';
        }

        if (!preg_match('/^[A-Z]{2}/', $cleaned)) {
            return 'IBAN muss mit einem Laendercode beginnen (z.B. DE).';
        }

        if (!preg_match('/^[A-Z]{2}[0-9]{2}/', $cleaned)) {
            return 'IBAN muss nach dem Laendercode zwei Pruefziffern enthalten.';
        }

        if (!preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/', $cleaned)) {
            return 'IBAN darf nur Buchstaben und Ziffern enthalten.';
        }

        $country = substr($cleaned, 0, 2);
        if (isset(self::COUNTRY_LENGTHS[$country])) {
            $expected = self::COUNTRY_LENGTHS[$country];
            if (strlen($cleaned) !== $expected) {
                return sprintf(
                    'IBAN fuer %s muss genau %d Zeichen lang sein (aktuell: %d).',
                    $country,
                    $expected,
                    strlen($cleaned)
                );
            }
        }

        if (!self::mod97Check($cleaned)) {
            return 'IBAN-Pruefsumme ist ungueltig.';
        }

        return null; // Valid
    }

    /**
     * Perform MOD-97 checksum validation per ISO 13616.
     *
     * 1. Move first 4 characters to end
     * 2. Convert letters to numbers (A=10, B=11, ..., Z=35)
     * 3. Compute number modulo 97
     * 4. Result must equal 1
     */
    private static function mod97Check(string $iban): bool {
        // Move first 4 chars (country + check digits) to end
        $rearranged = substr($iban, 4) . substr($iban, 0, 4);

        // Convert letters to numbers
        $numericStr = '';
        for ($i = 0; $i < strlen($rearranged); $i++) {
            $char = $rearranged[$i];
            if ($char >= 'A' && $char <= 'Z') {
                $numericStr .= (string)(ord($char) - 55);
            } else {
                $numericStr .= $char;
            }
        }

        // Process in chunks to avoid integer overflow
        $remainder = 0;
        for ($i = 0; $i < strlen($numericStr); $i++) {
            $remainder = ($remainder * 10 + (int)$numericStr[$i]) % 97;
        }

        return $remainder === 1;
    }

    /**
     * Strip all whitespace and convert to uppercase.
     */
    private static function clean(string $iban): string {
        return strtoupper(preg_replace('/\s+/', '', trim($iban)));
    }
}
