"""
Lightweight query interface for the ICD-10 code hierarchy.

Hierarchy: Chapter → Parent code (e.g. A00) → Child code (e.g. A00.1)

All public methods accept loose input and normalise it automatically.
See normalize_code() for the full set of accepted formats.
"""

import json
import re
from pathlib import Path

_DEFAULT_MAP_PATH = Path(__file__).parent / "data" / "icd10_map.json"

_RE_NEEDS_DOT   = re.compile(r"^([A-Z]\d{2})(\d[A-Z0-9]*)$")
_RE_CHAPTER_NUM = re.compile(r"^\d+$")

_ROMAN = [
    (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
    (100,  "C"), (90,  "XC"), (50,  "L"), (40,  "XL"),
    (10,   "X"), (9,   "IX"), (5,   "V"), (4,   "IV"),
    (1,    "I"),
]


def _to_roman(n: int) -> str:
    result = ""
    for value, numeral in _ROMAN:
        while n >= value:
            result += numeral
            n -= value
    return result


# ── Exceptions ────────────────────────────────────────────────────────────────

class ICD10Error(Exception):
    """Base class for all ICD-10 library exceptions."""

class CodeNotFoundError(ICD10Error):
    """Raised when a code or chapter key does not exist in the map.
    Attributes: code (str)
    """
    def __init__(self, code: str):
        super().__init__(f"Code not found: '{code}'")
        self.code = code

class NoParentError(ICD10Error):
    """Raised by get_parent() on a chapter key (top of hierarchy, no parent).
    Attributes: code (str)
    """
    def __init__(self, code: str):
        super().__init__(f"'{code}' has no parent (it is a top-level entry)")
        self.code = code

class NotAChapterError(ICD10Error):
    """Raised when a chapter-only method is called with a non-chapter key.
    Attributes: key (str)
    """
    def __init__(self, key: str):
        super().__init__(f"'{key}' is not a chapter key")
        self.key = key

class MapNotFoundError(ICD10Error):
    """Raised when the JSON map file is not found at initialisation.
    Attributes: path (Path)
    """
    def __init__(self, path: Path):
        super().__init__(f"ICD-10 map file not found: '{path}'")
        self.path = path


# ── Normalisation ─────────────────────────────────────────────────────────────

def normalize_code(code: str) -> str:
    """Return the canonical form of a user-supplied code or chapter identifier.

    "A001" → "A00.1" | "6" → "Chapter VI" | "chapter 6" → "Chapter VI"
    """
    code = code.strip()

    if code.lower().startswith("chapter"):
        rest = code[len("chapter"):].strip()
        if _RE_CHAPTER_NUM.match(rest):
            return f"Chapter {_to_roman(int(rest))}"
        return f"Chapter {rest.upper()}"

    if _RE_CHAPTER_NUM.match(code):
        return f"Chapter {_to_roman(int(code))}"

    code = code.upper()
    m = _RE_NEEDS_DOT.match(code)
    if m:
        return f"{m.group(1)}.{m.group(2)}"

    return code


# ── Main class ────────────────────────────────────────────────────────────────

class ICD10:
    """Query interface for the ICD-10 code hierarchy.

    Loads the bundled icd10_map.json by default.
    Raises MapNotFoundError if the file is not found.
    """

    def __init__(self, map_path: str | Path = _DEFAULT_MAP_PATH):
        path = Path(map_path)
        if not path.exists():
            raise MapNotFoundError(path)
        with open(path, encoding="utf-8") as f:
            self.db: dict = json.load(f)

    def _get_entry(self, code: str) -> dict:
        code = normalize_code(code)
        entry = self.db.get(code)
        if entry is None:
            raise CodeNotFoundError(code)
        return entry

    def get_description(self, code: str) -> str:
        """Return the human-readable description of a code or chapter key.
        Raises CodeNotFoundError if not found.
        """
        return self._get_entry(code)["description"]

    def code_exists(self, code: str) -> bool:
        """Return True if the code exists in the map, False otherwise."""
        return normalize_code(code) in self.db

    def get_parent(self, code: str) -> str:
        """Return the parent key of a code.
        Raises NoParentError for chapter keys, CodeNotFoundError if not found.
        """
        entry = self._get_entry(code)
        if entry["parent"] is None:
            raise NoParentError(normalize_code(code))
        return entry["parent"]

    def get_chapter(self, code: str) -> str:
        """Return the chapter key for any code at any level.
        Raises CodeNotFoundError if not found.
        """
        return self._get_entry(code)["chapter"]

    def get_children(self, code: str) -> list[str]:
        """Return direct children of a code. Returns [] for leaf nodes.
        Raises CodeNotFoundError if not found.
        """
        return self._get_entry(code)["children"]

    def get_range(self, chapter_key: str) -> str:
        """Return the code range of a chapter, e.g. 'A00–B99'.
        Raises NotAChapterError, CodeNotFoundError.
        """
        entry = self._get_entry(chapter_key)
        if entry["type"] != "chapter":
            raise NotAChapterError(normalize_code(chapter_key))
        return entry["range"]

    def list_chapter_codes(self, chapter_key: str) -> list[str]:
        """Return all parent and child codes belonging to a chapter.
        Raises NotAChapterError, CodeNotFoundError.
        """
        entry = self._get_entry(chapter_key)
        if entry["type"] != "chapter":
            raise NotAChapterError(normalize_code(chapter_key))
        chapter_key = normalize_code(chapter_key)
        return [
            code for code, e in self.db.items()
            if e.get("chapter") == chapter_key and e["type"] != "chapter"
        ]