# -*- coding: utf-8 -*-
"""DokoGen — Склонение (pymorphy3 + правила для ФИО/организаций)"""

from functools import lru_cache
from typing import Dict
import re

try:
    import pymorphy3
    morph = pymorphy3.MorphAnalyzer()
    MORPH_AVAILABLE = True
except ImportError:
    morph = None
    MORPH_AVAILABLE = False

CASES = ["nomn", "gent", "datv", "accs", "ablt", "loct"]
CASE_MAP = {
    "nominative": "nomn", "им": "nomn",
    "genitive": "gent", "род": "gent",
    "dative": "datv", "дат": "datv",
    "accusative": "accs", "вин": "accs",
    "instrumental": "ablt", "твор": "ablt",
    "prepositional": "loct", "пр": "loct",
}
CASE_NAMES = {
    "nomn": "Именительный (Кто? Что?)",
    "gent": "Родительный (Кого? Чего?)",
    "datv": "Дательный (Кому? Чему?)",
    "accs": "Винительный (Кого? Что?)",
    "ablt": "Творительный (Кем? Чем?)",
    "loct": "Предложный (О ком? О чём?)",
}
CASE_SHORT = {
    "nomn": "им.п.", "gent": "род.п.", "datv": "дат.п.",
    "accs": "вин.п.", "ablt": "твор.п.", "loct": "пр.п.",
}


def _normalize_case(case: str) -> str:
    return CASE_MAP.get(case, "nomn" if case not in CASES else case)


def inflect_name(name: str) -> Dict[str, str]:
    """Склонение названия организации по всем падежам."""
    return {gr: company_name_decline(name, gr) for gr in CASES}


@lru_cache(maxsize=512)
def company_name_decline(name: str, case: str) -> str:
    """Склонение названия организации."""
    if not name:
        return ""
    case_gr = _normalize_case(case)
    if case_gr == "nomn":
        return name

    m = re.match(r'^(.+?)\s*([\u00ab\"].*[\u00bb\"])$', name.strip())
    if m:
        prefix = m.group(1).strip()
        quoted = m.group(2)
        if prefix:
            return decline(prefix, case_gr, decline_all=True) + " " + quoted
        return name
    return decline(name, case_gr, decline_all=True)


@lru_cache(maxsize=512)
def decline(text: str, case: str, decline_all: bool = False) -> str:
    """Склонение строки (ФИО, фразы) по падежам."""
    if not text:
        return ""
    case_gr = _normalize_case(case)
    if case_gr == "nomn":
        return text

    clean_text, quoted = _extract_quoted(text)
    tokens = re.split(r"(\s+)", clean_text)
    content_tokens = [t for t in tokens if not t.isspace() and t.strip()]

    only_first = False
    if not decline_all and len(content_tokens) >= 2:
        for ct in content_tokens[1:]:
            if ct[0].islower():
                only_first = True
                break

    result_parts = []
    content_idx = 0
    for token in tokens:
        if token.isspace() or not token.strip():
            result_parts.append(token)
            continue
        stripped = token.strip()
        if only_first and content_idx > 0:
            result_parts.append(token)
            content_idx += 1
            continue
        if _is_quoted(stripped):
            result_parts.append(token)
            content_idx += 1
            continue
        if stripped.isupper() and len(stripped) > 1:
            result_parts.append(token)
            content_idx += 1
            continue
        inflected = _inflect_word(stripped, case_gr)
        if stripped[0].isupper():
            inflected = inflected[0].upper() + inflected[1:]
        result_parts.append(inflected)
        content_idx += 1

    return _restore_quoted("".join(result_parts), quoted)


def _inflect_word(word: str, case_gr: str) -> str:
    """Склонение одного слова через pymorphy3 с fallback на ручные правила."""
    if not word or len(word) < 2 or case_gr == "nomn":
        return word
    if not MORPH_AVAILABLE or morph is None:
        return _rules_decline(word, case_gr) or word
    try:
        parsed = morph.parse(word)
        if not parsed:
            return _rules_decline(word, case_gr) or word
        best = parsed[0]
        inflected = best.inflect({case_gr})
        if inflected:
            return inflected.word
        return _rules_decline(word, case_gr) or word
    except Exception:
        return _rules_decline(word, case_gr) or word


def _rules_decline(word: str, case_gr: str) -> str:
    """Ручное склонение фамилий."""
    w = word.lower()
    # Мужские фамилии на -ов/-ев/-ёв
    if w.endswith(('ов', 'ев', 'ёв')) and not w.endswith(('ова', 'ева', 'ёва')):
        return {
            'gent': word + 'а', 'datv': word + 'у', 'accs': word + 'а',
            'ablt': word + 'ым', 'loct': word + 'е'
        }.get(case_gr, word)
    # Мужские фамилии на -ин
    if w.endswith('ин') and not w.endswith('ина'):
        return {
            'gent': word + 'а', 'datv': word + 'у', 'accs': word + 'а',
            'ablt': word + 'ым', 'loct': word + 'е'
        }.get(case_gr, word)
    # Женские фамилии на -ова/-ева/-ёва
    if w.endswith(('ова', 'ева', 'ёва')):
        stem = word[:-1]
        return {
            'gent': stem + 'ой', 'datv': stem + 'ой', 'accs': stem + 'у',
            'ablt': stem + 'ой', 'loct': stem + 'ой'
        }.get(case_gr, word)
    # Женские фамилии на -ина
    if w.endswith('ина'):
        stem = word[:-1]
        return {
            'gent': stem + 'ой', 'datv': stem + 'ой', 'accs': stem + 'у',
            'ablt': stem + 'ой', 'loct': stem + 'ой'
        }.get(case_gr, word)
    return word


def _extract_quoted(text: str):
    """Извлекает подстроки в кавычках, заменяет плейсхолдерами."""
    if not text:
        return text, {}
    pattern = re.compile(r'(\u00ab[^\u00bb]*\u00bb|"[^"]*")')
    placeholders = {}
    counter = [0]

    def _replace(m):
        ph = f'__Q_{counter[0]}__'
        placeholders[ph] = m.group(0)
        counter[0] += 1
        return ph

    return pattern.sub(_replace, text), placeholders


def _restore_quoted(text: str, placeholders: dict) -> str:
    for ph, original in placeholders.items():
        text = text.replace(ph, original)
    return text


def _is_quoted(word: str) -> bool:
    w = word.strip()
    if not w:
        return False
    return (w.startswith("\u00ab") and w.endswith("\u00bb")) or \
           (w.startswith('"') and w.endswith('"'))


# ==================== ФОРМАТИРОВАНИЕ ФИО ====================

def _initial(word: str) -> str:
    """Инициал из слова: если уже с точкой — вернуть как есть,
    иначе первая буква + точка. «В.Н.» → «В.Н.», «Владимир» → «В.»"""
    w = word.strip()
    if not w:
        return ''
    if w.endswith('.'):
        return w
    return w[0] + '.'


def get_short_fio(fio: str) -> str:
    """Фамилия И.О."""
    if not fio:
        return ""
    parts = fio.split()
    if len(parts) >= 2:
        initials = _initial(parts[1])
        if len(parts) >= 3:
            initials += _initial(parts[2])
        return parts[0] + " " + initials
    return fio


def get_initials(fio: str) -> str:
    """И.О. Фамилия"""
    if not fio:
        return ""
    parts = fio.split()
    if len(parts) >= 2:
        initials = _initial(parts[1])
        if len(parts) >= 3:
            initials += _initial(parts[2])
        return initials + " " + parts[0]
    return fio
