"""
pan
===

Utilities for validating and extracting information from Indian PAN
(Permanent Account Number) cards.

This module handles:
- Format validation (Regex)
- Entity Type Extraction (P, C, H, F, etc.)
- Cross-referencing PAN embedded within GSTINs.

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

import re


_PAN_REGEX = re.compile(r"^[A-Z]{3}[ABCFGHLJPTK][A-Z]\d{4}[A-Z]$")

# Official Income Tax Department mapping for the 4th character
_ENTITY_TYPES = {
    "P": "Individual",
    "C": "Company",
    "H": "Hindu Undivided Family (HUF)",
    "A": "Association of Persons (AOP)",
    "B": "Body of Individuals (BOI)",
    "G": "Government Agency",
    "J": "Artificial Juridical Person",
    "L": "Local Authority",
    "F": "Firm / Limited Liability Partnership",
    "T": "Trust",
    "K": "Krishn (Trust)",  # Rare, but exists
}

class PANError(ValueError):
    """Custom exception for PAN validation errors."""
    pass


def is_valid_pan_format(pan: str) -> bool:
    """
    Validates if the string strictly matches the PAN structure.

    Parameters
    ----------
    pan : str
        The 10-character alphanumeric string.

    Returns
    -------
    bool
        True if valid pattern, False otherwise.
    """
    if not pan or len(pan) != 10:
        return False
    
    # Ensure uppercase before regex check
    return bool(_PAN_REGEX.match(pan.upper()))


def extract_pan_type(pan: str) -> str:
    """
    Decodes the 4th character of the PAN to determine the entity type.

    Parameters
    ----------
    pan : str
        A valid PAN string.

    Returns
    -------
    str
        Description of the entity (e.g., 'Individual', 'Company').
        Returns 'Unknown Entity' if the 4th char is invalid but format is okay.

    Raises
    ------
    PANError
        If the PAN format is invalid.
    """
    if not is_valid_pan_format(pan):
        raise PANError(f"Invalid PAN format: '{pan}'")
    
    # The 4th character (index 3) holds the entity status
    status_char = pan[3].upper()
    
    return _ENTITY_TYPES.get(status_char, "Unknown Entity")


def validate_pan_from_gstin(gstin: str) -> bool:
    """
    Extracts the PAN from a GSTIN and validates its internal structure.
    
    A GSTIN is essentially: StateCode (2) + PAN (10) + EntityCode (1) + Z + Checksum (1).
    Therefore, characters 2 through 12 MUST be a valid PAN.

    Parameters
    ----------
    gstin : str
        The 15-character GSTIN string.

    Returns
    -------
    bool
        True if the embedded PAN is valid.
    """
    if not gstin or len(gstin) != 15:
        return False

    # Extract chars at index 2 to 12 (10 chars total)
    embedded_pan = gstin[2:12]

    return is_valid_pan_format(embedded_pan)


def get_pan_metadata(pan: str) -> dict:
    """
    Returns a dictionary of all extractable information from a PAN.
    """
    try:
        entity_type = extract_pan_type(pan)
        is_valid = True
        error = None
    except PANError:
        entity_type = None
        is_valid = False
        error = "Invalid Format"

    return {
        "pan": pan.upper() if pan else None,
        "valid": is_valid,
        "entity_type": entity_type,
        "category_code": pan[3].upper() if is_valid else None,
        "surname_initial": pan[4].upper() if is_valid else None,
        "error": error
    }