"""
gstin
=====

A robust utility for validating, parsing, and cleaning GSTIN 
(Goods and Services Tax Identification Number) codes for Indian businesses.

This module handles:
- Format validation (Regex)
- Checksum validation (Luhn Mod 36 - Official Left-to-Right Weighting)
- OCR Error Correction (Context-aware)
- Component extraction (State, PAN, Entity Code)

:copyright: (c) 2026
:license: GPL-3.0-only
"""

import re
import string

# --- CONSTANTS ---

_CHAR_MAP = {char: i for i, char in enumerate(string.digits + string.ascii_uppercase)}
_REV_CHAR_MAP = {i: char for i, char in enumerate(string.digits + string.ascii_uppercase)}
# Revised Regex: Position 13 is usually 'Z', Position 14 is the checksum
_GSTIN_REGEX = re.compile(r"^\d{2}[A-Z]{5}\d{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$")

_NUMERIC_FIXES = {"O": "0", "I": "1", "L": "1", "Z": "2", "S": "5", "B": "8"}
_ALPHA_FIXES = {"0": "O", "1": "I", "2": "Z", "5": "S", "8": "B"}


class GSTINError(ValueError):
    """Base custom exception for GSTIN errors."""
    pass


def _mod36_checksum(body: str) -> str:
    """
    Calculates the Mod 36 checksum using the official GST weightage.
    
    Logic:
    - Multiplier 1 for odd positions, 2 for even (1-indexed, left-to-right).
    - Add (Product // 36) and (Product % 36) to the total.
    """
    total = 0
    mod = 36

    for i, char in enumerate(body):
        try:
            code_point = _CHAR_MAP[char]
        except KeyError:
            raise GSTINError(f"Invalid character '{char}' in GSTIN body.")

        # Left-to-right weightage: 1st char=1, 2nd char=2, 3rd char=1...
        multiplier = 1 if (i + 1) % 2 != 0 else 2
        product = code_point * multiplier
        
        # Sum of digits logic in Base 36
        addend = (product // mod) + (product % mod)
        total += addend

    remainder = total % mod
    check_code_point = (mod - remainder) % mod
    
    return _REV_CHAR_MAP[check_code_point]


def repair_gstin(gstin: str) -> str:
    """
    Intelligently repairs a GSTIN string corrupted by OCR.
    
    Uses positional context:
    - Positions 0,1: State (Numeric)
    - Positions 7,8,9,10: PAN Numeric (Numeric)
    - Position 12: Entity Number (Numeric/Alpha, often 1 vs I)
    """
    if not gstin:
        return ""

    cleaned = gstin.upper().strip()
    cleaned = re.sub(r"[^A-Z0-9]", "", cleaned)
    
    if len(cleaned) != 15:
        return cleaned

    gstin_list = list(cleaned)

    for i, char in enumerate(gstin_list):
        # Numeric required zones
        if i in [0, 1, 7, 8, 9, 10, 12]:
            if char in _NUMERIC_FIXES:
                gstin_list[i] = _NUMERIC_FIXES[char]
        
        # Alpha required zones
        elif i in [2, 3, 4, 5, 6, 11, 13]:
            if char in _ALPHA_FIXES:
                gstin_list[i] = _ALPHA_FIXES[char]

    return "".join(gstin_list)


def validate(gstin: str) -> bool:
    """
    Full validation: Format check + Checksum verification.
    """
    if not gstin or len(gstin) != 15:
        return False
    
    # 1. Regex check (Structure)
    if not _GSTIN_REGEX.match(gstin):
        return False

    # 2. Checksum check (Math)
    try:
        # Calculate expected based on first 14 chars
        expected = _mod36_checksum(gstin[:14])
        return gstin[14] == expected
    except GSTINError:
        return False


def get_metadata(gstin: str) -> dict:
    """Extracts metadata components from the GSTIN string."""
    is_valid = validate(gstin)
    
    if len(gstin) < 14:
        return {"valid": False, "error": "Insufficient length"}

    return {
        "valid": is_valid,
        "state_code": gstin[:2],
        "pan": gstin[2:12],
        "entity_code": gstin[12],
        "check_digit": gstin[14] if len(gstin) == 15 else None
    }