from __future__ import annotations

import re
from typing import Dict, Optional


DOI_REGEX = re.compile(r"10\.\d{4,9}/[^\s\"<>]+", re.IGNORECASE)
ARXIV_REGEX = re.compile(
    r"(?:arxiv:)?(?P<id>\d{4}\.\d{4,5}|[a-z\-]+/\d{7})",
    re.IGNORECASE,
)
ISBN_REGEX = re.compile(r"(?P<isbn>(?:97[89][\-\s]?)?\d[\d\-\s]{8,}\d[\dXx])")


def _strip_trailing_punct(value: str) -> str:
    return value.rstrip(".,;:()[]{}<>\"")


def extract_doi(text: str) -> Optional[str]:
    match = DOI_REGEX.search(text)
    if not match:
        return None
    return _strip_trailing_punct(match.group(0))


def extract_arxiv(text: str) -> Optional[str]:
    match = ARXIV_REGEX.search(text)
    if not match:
        return None
    return match.group("id")


def _isbn_check_digit(isbn_digits: str) -> bool:
    if len(isbn_digits) == 10:
        total = 0
        for idx, char in enumerate(isbn_digits[:9], start=1):
            if not char.isdigit():
                return False
            total += idx * int(char)
        check = total % 11
        expected = "X" if check == 10 else str(check)
        return isbn_digits[-1].upper() == expected
    if len(isbn_digits) == 13 and isbn_digits.isdigit():
        total = 0
        for idx, char in enumerate(isbn_digits[:12]):
            total += int(char) * (1 if idx % 2 == 0 else 3)
        check = (10 - (total % 10)) % 10
        return isbn_digits[-1] == str(check)
    return False


def extract_isbn(text: str) -> Optional[str]:
    for match in ISBN_REGEX.finditer(text):
        raw = match.group("isbn")
        digits = re.sub(r"[^0-9Xx]", "", raw)
        if len(digits) in {10, 13} and _isbn_check_digit(digits):
            return digits.upper()
    return None


def extract_ids(text: str) -> Dict[str, str]:
    ids: Dict[str, str] = {}
    doi = extract_doi(text)
    if doi:
        ids["doi"] = doi
    arxiv = extract_arxiv(text)
    if arxiv:
        ids["arxiv"] = arxiv
    isbn = extract_isbn(text)
    if isbn:
        ids["isbn"] = isbn
    return ids
