from __future__ import annotations

import re
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal, InvalidOperation
from html import escape
from zoneinfo import ZoneInfo

KYIV = ZoneInfo("Europe/Kyiv")
WEEKDAYS_UK = ("понеділок", "вівторок", "середа", "четвер", "пʼятниця", "субота", "неділя")


def utcnow() -> datetime:
    return datetime.now(timezone.utc)


def format_money(kopiyky: int) -> str:
    sign = "-" if kopiyky < 0 else ""
    hryvnia, kop = divmod(abs(int(kopiyky)), 100)
    if kop == 0:
        return f"{sign}{hryvnia} грн"
    return f"{sign}{hryvnia},{kop:02d} грн"


def uah_to_kop(value: str) -> int:
    normalized = value.replace(",", ".").strip()
    try:
        amount = Decimal(normalized).quantize(Decimal("0.01"))
    except InvalidOperation as exc:
        raise ValueError("Некоректна ціна") from exc
    if amount < 0 or amount > Decimal("100000"):
        raise ValueError("Некоректна ціна")
    return int(amount * 100)


def kop_to_input(kopiyky: int) -> str:
    return f"{Decimal(kopiyky) / Decimal(100):.2f}"


def format_kyiv(value: datetime | None) -> str:
    if value is None:
        return ""
    if value.tzinfo is None:
        value = value.replace(tzinfo=timezone.utc)
    return value.astimezone(KYIV).strftime("%d.%m.%Y %H:%M")


def format_visit_date(day: date) -> str:
    return f"{WEEKDAYS_UK[day.weekday()]}, {day.strftime('%d.%m.%Y')}"


def setting_int(settings: dict[str, str], key: str, default: int, low: int, high: int) -> int:
    try:
        value = int(str(settings.get(key, default)).strip())
    except ValueError:
        return default
    return max(low, min(high, value))


def apply_discount(subtotal: int, percent: int) -> tuple[int, int]:
    percent = max(0, min(int(percent), 90))
    if subtotal <= 0 or percent == 0:
        return 0, max(subtotal, 0)
    discount = subtotal * percent // 100
    return discount, subtotal - discount


def normalize_phone(raw: str) -> str | None:
    text = raw.strip()
    digits = re.sub(r"\D", "", text)
    if len(digits) == 12 and digits.startswith("380"):
        return "+" + digits
    if len(digits) == 11 and digits.startswith("80"):
        return "+3" + digits
    if len(digits) == 10 and digits.startswith("0"):
        return "+38" + digits
    if text.startswith("+") and 10 <= len(digits) <= 15:
        return "+" + digits
    return None


def clean_name(text: str) -> str | None:
    cleaned = " ".join(text.replace("’", "'").replace("`", "'").split())
    if len(cleaned) < 5 or len(cleaned) > 80:
        return None
    parts = cleaned.split(" ")
    if len(parts) < 2:
        return None
    pattern = re.compile(r"[A-Za-zА-Яа-яІіЇїЄєҐґ'’\-]{2,}")
    if not all(pattern.fullmatch(part) for part in parts):
        return None
    return cleaned


def parse_visit_date(text: str, today: date | None = None) -> date | None:
    today = today or datetime.now(KYIV).date()
    raw = text.strip()
    parsed: date | None = None
    for fmt in ("%d.%m.%Y", "%d.%m.%y"):
        try:
            parsed = datetime.strptime(raw, fmt).date()
            break
        except ValueError:
            continue
    if parsed is None:
        parts = raw.split(".")
        if len(parts) != 2 or not all(part.isdigit() for part in parts):
            return None
        day, month = int(parts[0]), int(parts[1])
        try:
            parsed = date(today.year, month, day)
        except ValueError:
            return None
        if parsed < today:
            try:
                parsed = date(today.year + 1, month, day)
            except ValueError:
                return None
    if parsed < today or parsed > today + timedelta(days=90):
        return None
    return parsed


def parse_time(text: str) -> str | None:
    match = re.fullmatch(r"([01]?\d|2[0-3])[:.]([0-5]\d)", text.strip())
    if not match:
        return None
    return f"{int(match.group(1)):02d}:{match.group(2)}"


def parse_guests(text: str) -> int | None:
    raw = text.strip()
    if not raw.isdigit():
        return None
    count = int(raw)
    if 1 <= count <= 20:
        return count
    return None


def render_template(template: str, **kwargs: object) -> str:
    class Safe(dict):
        def __missing__(self, key: str) -> str:
            return "{" + key + "}"

    escaped = {key: escape(str(value)) for key, value in kwargs.items()}
    try:
        return template.format_map(Safe(escaped))
    except ValueError:
        return template


def receipt_text(items: list[dict], discount_percent: int) -> tuple[str, int, int, int]:
    lines: list[str] = []
    subtotal = 0
    for item in items:
        title = escape(str(item.get("title") or ""))
        qty = int(item.get("qty") or 1)
        price = int(item.get("price") or 0)
        if price > 0:
            subtotal += price * qty
            lines.append(f"• {title} × {qty} — {format_money(price * qty)}")
        else:
            lines.append(f"• {title}")
    discount, total = apply_discount(subtotal, discount_percent if subtotal else 0)
    return "\n".join(lines), subtotal, discount, total


def clip(text: str, limit: int = 60) -> str:
    text = " ".join(text.split())
    if len(text) <= limit:
        return text
    return text[: limit - 1] + "…"
