# handelsregisternummer.py - functions for handling German company registry id
# coding: utf-8
#
# Copyright (C) 2015 Holvi Payment Services Oy
# Copyright (C) 2018-2025 Arthur de Jong
#
# 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

"""Handelsregisternummer (German company register number).

The number consists of the court where the company has registered, the type
of register and the registration number.

The type of the register is either HRA or HRB where the letter "B" stands for
HR section B, where limited liability companies and corporations are entered
(GmbH's and AG's). There is also a section HRA for business partnerships
(OHG's, KG's etc.). In other words: businesses in section HRB are limited
liability companies, while businesses in HRA have personally liable partners.

More information:

* https://www.handelsregister.de/
* https://en.wikipedia.org/wiki/German_Trade_Register
* https://offeneregister.de/

>>> validate('Aachen HRA 11223')
'Aachen HRA 11223'
>>> validate('Frankfurt/Oder GnR 11223', company_form='e.G.')
'Frankfurt/Oder GnR 11223'
>>> validate('Aachen HRC 44123')
Traceback (most recent call last):
  ...
InvalidFormat: ...
>>> validate('Aachen HRA 44123', company_form='GmbH')
Traceback (most recent call last):
  ...
InvalidComponent: ...
"""

from __future__ import annotations

import re
import unicodedata

from stdnum.exceptions import *
from stdnum.util import clean


TYPE_CHECKING = False
if TYPE_CHECKING:  # pragma: no cover (typechecking only import)
    from typing import Any


# The known courts that have a Handelsregister
GERMAN_COURTS = (
    'Aachen',
    'Altenburg',
    'Amberg',
    'Ansbach',
    'Apolda',
    'Arnsberg',
    'Arnstadt Zweigstelle Ilmenau',
    'Arnstadt',
    'Aschaffenburg',
    'Augsburg',
    'Aurich',
    'Bad Hersfeld',
    'Bad Homburg v.d.H.',
    'Bad Kreuznach',
    'Bad Oeynhausen',
    'Bad Salzungen',
    'Bamberg',
    'Bayreuth',
    'Berlin (Charlottenburg)',
    'Bielefeld',
    'Bochum',
    'Bonn',
    'Braunschweig',
    'Bremen',
    'Chemnitz',
    'Coburg',
    'Coesfeld',
    'Cottbus',
    'Darmstadt',
    'Deggendorf',
    'Dortmund',
    'Dresden',
    'Duisburg',
    'Düren',
    'Düsseldorf',
    'Eisenach',
    'Erfurt',
    'Eschwege',
    'Essen',
    'Flensburg',
    'Frankfurt am Main',
    'Frankfurt/Oder',
    'Freiburg',
    'Friedberg',
    'Fritzlar',
    'Fulda',
    'Fürth',
    'Gelsenkirchen',
    'Gera',
    'Gießen',
    'Gotha',
    'Greiz',
    'Göttingen',
    'Gütersloh',
    'Hagen',
    'Hamburg',
    'Hamm',
    'Hanau',
    'Hannover',
    'Heilbad Heiligenstadt',
    'Hildburghausen',
    'Hildesheim',
    'Hof',
    'Homburg',
    'Ingolstadt',
    'Iserlohn',
    'Jena',
    'Kaiserslautern',
    'Kassel',
    'Kempten (Allgäu)',
    'Kiel',
    'Kleve',
    'Koblenz',
    'Korbach',
    'Krefeld',
    'Köln',
    'Königstein',
    'Landau',
    'Landshut',
    'Langenfeld',
    'Lebach',
    'Leipzig',
    'Lemgo',
    'Limburg',
    'Ludwigshafen a.Rhein (Ludwigshafen)',
    'Lübeck',
    'Lüneburg',
    'Mainz',
    'Mannheim',
    'Marburg',
    'Meiningen',
    'Memmingen',
    'Merzig',
    'Montabaur',
    'Mönchengladbach',
    'Mühlhausen',
    'München',
    'Münster',
    'Neubrandenburg',
    'Neunkirchen',
    'Neuruppin',
    'Neuss',
    'Nordhausen',
    'Nürnberg',
    'Offenbach am Main',
    'Oldenburg (Oldenburg)',
    'Osnabrück',
    'Ottweiler',
    'Paderborn',
    'Passau',
    'Pinneberg',
    'Potsdam',
    'Pößneck Zweigstelle Bad Lobenstein',
    'Pößneck',
    'Recklinghausen',
    'Regensburg',
    'Rostock',
    'Rudolstadt Zweigstelle Saalfeld',
    'Rudolstadt',
    'Saarbrücken',
    'Saarlouis',
    'Schweinfurt',
    'Schwerin',
    'Siegburg',
    'Siegen',
    'Sondershausen',
    'Sonneberg',
    'St. Ingbert (St Ingbert)',
    'St. Wendel (St Wendel)',
    'Stadthagen',
    'Stadtroda',
    'Steinfurt',
    'Stendal',
    'Stralsund',
    'Straubing',
    'Stuttgart',
    'Suhl',
    'Sömmerda',
    'Tostedt',
    'Traunstein',
    'Ulm',
    'Völklingen',
    'Walsrode',
    'Weiden i. d. OPf.',
    'Weimar',
    'Wetzlar',
    'Wiesbaden',
    'Wittlich',
    'Wuppertal',
    'Würzburg',
    'Zweibrücken',
)


def _to_min(court: str) -> str:
    """Convert the court name for quick comparison without encoding issues."""
    return ''.join(
        x for x in unicodedata.normalize('NFD', court.lower())
        if x in 'abcdefghijklmnopqrstuvwxyz')


# Build a dictionary for lookup up courts
_courts = dict(
    (_to_min(court), court) for court in GERMAN_COURTS)
_courts.update(
    (_to_min(alias), court) for alias, court in (
        ('Allgäu', 'Kempten (Allgäu)'),
        ('Bad Homburg', 'Bad Homburg v.d.H.'),
        ('Berlin', 'Berlin (Charlottenburg)'),
        ('Charlottenburg', 'Berlin (Charlottenburg)'),
        ('Charlottenburg (Berlin)', 'Berlin (Charlottenburg)'),
        ('Kaln', 'Köln'),  # for encoding issues
        ('Kempten', 'Kempten (Allgäu)'),
        ('Ludwigshafen am Rhein (Ludwigshafen)', 'Ludwigshafen a.Rhein (Ludwigshafen)'),
        ('Ludwigshafen am Rhein', 'Ludwigshafen a.Rhein (Ludwigshafen)'),
        ('Ludwigshafen', 'Ludwigshafen a.Rhein (Ludwigshafen)'),
        ('Oldenburg', 'Oldenburg (Oldenburg)'),
        ('St. Ingbert', 'St. Ingbert (St Ingbert)'),
        ('St. Wendel', 'St. Wendel (St Wendel)'),
        ('Weiden in der Oberpfalz', 'Weiden i. d. OPf.'),
        ('Weiden', 'Weiden i. d. OPf.'),
        ('Paderborn früher Höxter', 'Paderborn'),
    ))


# The known registry types
REGISTRY_TYPES = (
    'HRA',
    'HRB',
    'PR',
    'GnR',
    'VR',
)

COMPANY_FORM_REGISTRY_TYPES = {
    'e.K.': 'HRA',
    'e.V.': 'VR',
    'Verein': 'VR',
    'OHG': 'HRA',
    'KG': 'HRA',
    'KGaA': 'HRB',
    'Vor-GmbH': 'HRB',
    'GmbH': 'HRB',
    'UG': 'HRB',
    'UG i.G.': 'HRB',
    'AG': 'HRB',
    'e.G.': 'GnR',
    'PartG': 'PR',
}


# possible formats the number can be specified in
_court_re = r'(?P<court>.*)'
_registry_re = r'(?P<registry>%s)' % '|'.join(REGISTRY_TYPES)
_number_re = r'(?P<nr>[1-9][0-9]{0,5})(\s*(?P<x>[A-ZÖ]{1,3}))?'
_formats = [
    _registry_re + r'\s+' + _number_re + r',?\s+' + _court_re + '$',
    _court_re + r',?\s+' + _registry_re + r'\s+' + _number_re + '$',
]


def _split(number: str) -> tuple[str, str, str, str | None]:
    """Split the number into a court, registry, register number and
    optionally qualifier."""
    number = clean(number).strip()
    for fmt in _formats:
        m = re.match(fmt, number, flags=re.I | re.U)
        if m:
            return m.group('court').strip(), m.group('registry'), m.group('nr'), m.group('x')
    raise InvalidFormat()


def compact(number: str) -> str:
    """Convert the number to the minimal representation. This strips the
    number of any valid separators and removes surrounding whitespace."""
    court, registry, number, qualifier = _split(number)
    return ' '.join(x for x in [court, registry, number, qualifier] if x)


def validate(number: str, company_form: str | None = None) -> str:
    """Check if the number is a valid company registry number. If a
    company_form (eg. GmbH or PartG) is given, the number is validated to
    have the correct registry type."""
    court: str | None
    court, registry, number, qualifier = _split(number)
    court = _courts.get(_to_min(court))
    if not court:
        raise InvalidComponent()
    if company_form and COMPANY_FORM_REGISTRY_TYPES.get(company_form) != registry:
        raise InvalidComponent()
    return ' '.join(x for x in [court, registry, number, qualifier] if x)


def is_valid(number: str) -> bool:
    """Check if the number is a valid company registry number."""
    try:
        return bool(validate(number))
    except ValidationError:
        return False


# The base URL for performing lookups
_offeneregister_url = 'https://db.offeneregister.de/openregister.json'


def check_offeneregister(
    number: str,
    timeout: float = 30,
    verify: bool | str = True,
) -> dict[str, Any] | None:  # pragma: no cover (not part of normal test suite)
    """Retrieve registration information from the OffeneRegister.de web site.

    The `timeout` argument specifies the network timeout in seconds.

    The `verify` argument is either a boolean that determines whether the
    server's certificate is validate or a string which must be a path the CA
    certificate bundle to use for verification.

    This basically returns the JSON response from the web service as a dict.
    It will contain something like the following::

        {
            'companyId': 'U1206_HRB14011',
            'courtCode': 'U1206',
            'courtName': 'Chemnitz',
            'globalId': 'sn_293298',
            'isCurrent': 'True',
            'name': 'Internet hier GmbH Presentation Provider',
            'nativeReferenceNumber': 'Chemnitz HRB 14011',
            'referenceNumberFirstSeen': '2020-06-12',
            'stdRefNo': 'U1206_HRB14011',
            'validFrom': '2020-06-12',
            'validTill': '',
        }

    Will return None if the number is invalid or unknown.
    """
    # this function isn't automatically tested because it would require
    # network access for the tests and unnecessarily load the web service
    import requests
    court, registry, number, qualifier = _split(number)
    response = requests.get(
        _offeneregister_url,
        params={
            'sql': '''
                   select *
                   from ReferenceNumbers
                   join Names on ReferenceNumbers.companyId = Names.companyId
                   where nativeReferenceNumber = :p0
                   limit 1
                   ''',
            'p0': '%s %s %s' % (court, registry, number)},
        timeout=timeout,
        verify=verify)
    response.raise_for_status()
    try:
        json = response.json()
        return dict(zip(json['columns'], json['rows'][0]))
    except (KeyError, IndexError) as e:  # noqa: F841
        return None  # number not found
