# gstin.py - functions for handling Indian VAT numbers
#
# Copyright (C) 2021 Gaurav Chauhan
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

"""GSTIN (Goods and Services Tax identification number, Indian VAT number).

The Goods and Services Tax identification number (GSTIN) is a 15 digit unique
identifier assigned to all business entities in India registered under the
Goods and Services Tax (GST) Act, 2017.

Each GSTIN begins with a 2 digit state code, the next 10 characters are the
holder's PAN, the 13th character is an alphanumeric digit that represents the
number of GSTIN registrations made in a state or union territory for same the
PAN, the 14th character is 'Z' and the last character is an alphanumeric
check digit calculated using Luhn mod 36 algorithm.

More information:

* https://bajajfinserv.in/insights/what-is-goods-and-service-tax-identification-number
* https://ddvat.gov.in/docs/List%20of%20State%20Code.pdf
* https://en.wikipedia.org/wiki/Goods_and_Services_Tax_(India)

>>> validate('27AAPFU0939F1ZV')
'27AAPFU0939F1ZV'
>>> validate('27AAPFU0939F1Z')
Traceback (most recent call last):
    ...
InvalidLength: ...
>>> validate('369296450896540')
Traceback (most recent call last):
    ...
InvalidFormat: ...
>>> validate('27AAPFU0939F1AA')
Traceback (most recent call last):
    ...
InvalidComponent: ...
>>> validate('27AAPFU0939F1ZO')
Traceback (most recent call last):
    ...
InvalidChecksum: ...
>>> to_pan('27AAPFU0939F1ZV')
'AAPFU0939F'
>>> info('27AAPFU0939F1ZV')['state']
'Maharashtra'
"""

from __future__ import annotations

import re

from stdnum import luhn
from stdnum.exceptions import *
from stdnum.in_ import pan
from stdnum.util import clean


_GSTIN_RE = re.compile(r'^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]{3}$')

_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'

_STATE_CODES = {
    '01': 'Jammu and Kashmir',
    '02': 'Himachal Pradesh',
    '03': 'Punjab',
    '04': 'Chandigarh',
    '05': 'Uttarakhand',
    '06': 'Haryana',
    '07': 'Delhi',
    '08': 'Rajasthan',
    '09': 'Uttar Pradesh',
    '10': 'Bihar',
    '11': 'Sikkim',
    '12': 'Arunachal Pradesh',
    '13': 'Nagaland',
    '14': 'Manipur',
    '15': 'Mizoram',
    '16': 'Tripura',
    '17': 'Meghalaya',
    '18': 'Assam',
    '19': 'West Bengal',
    '20': 'Jharkhand',
    '21': 'Orissa',
    '22': 'Chattisgarh',
    '23': 'Madhya Pradesh',
    '24': 'Gujarat',
    '25': 'Daman and Diu',
    '26': 'Dadar and Nagar Haveli',
    '27': 'Maharashtra',
    '28': 'Andhra Pradesh',
    '29': 'Karnataka',
    '30': 'Goa',
    '31': 'Lakshadweep',
    '32': 'Kerala',
    '33': 'Tamil Nadu',
    '34': 'Puducherry',
    '35': 'Anadaman and Nicobar Islands',
    '36': 'Telangana',
    '37': 'Andhra Pradesh (New)',
}


def compact(number: str) -> str:
    """Convert the number to the minimal representation. This strips the
    number of any valid separators and removes surrounding whitespace."""
    return clean(number, ' -').upper().strip()


def validate(number: str) -> str:
    """Check if the number provided is a valid GSTIN. This checks the length,
    formatting and check digit."""
    number = compact(number)
    if len(number) != 15:
        raise InvalidLength()
    if not _GSTIN_RE.match(number):
        raise InvalidFormat()
    if number[:2] not in _STATE_CODES or number[12] == '0' or number[13] != 'Z':
        raise InvalidComponent()
    pan.validate(number[2:12])
    luhn.validate(number, _ALPHABET)
    return number


def is_valid(number: str) -> bool:
    """Check if the number provided is a valid GSTIN. This checks the length,
    formatting and check digit."""
    try:
        return bool(validate(number))
    except ValidationError:
        return False


def to_pan(number: str) -> str:
    """Convert the number to a PAN."""
    number = compact(number)
    return number[2:12]


def info(number: str) -> dict[str, str | int | None]:
    """Provide information that can be decoded locally from GSTIN (without
    API)."""
    number = validate(number)
    return {
        'state': _STATE_CODES.get(number[:2]),
        'pan': number[2:12],
        'holder_type': pan.info(number[2:12])['holder_type'],
        'initial': number[6],
        'registration_count': _ALPHABET.index(number[12]),
    }
