"""
Price lookup: fetch a coin's Toman/Tether price from three independent
sources (Nobitex, BitPin, Geevex) and render a combined message.
"""

import re
import threading
import time
from datetime import datetime, timedelta, timezone

import requests

from .. import config

# Iran has used a fixed UTC+03:30 offset (no DST) since 1403/1404 (2022) —
# safe to hard-code rather than depend on a tzdata package on shared hosting.
_TEHRAN_OFFSET = timedelta(hours=3, minutes=30)

_cache_lock = threading.Lock()
_cache = {}  # url -> (fetched_at, payload)


def _cached_get(url: str):
    """GET url with a short TTL cache shared by every caller/coin/user.

    Each source endpoint returns *all* coins in one response, so caching the
    raw payload (rather than per-coin) means one fetch serves every symbol
    and every user until it expires — this is what keeps group traffic from
    multiplying outbound API calls.
    """
    now = time.time()
    with _cache_lock:
        hit = _cache.get(url)
        if hit and (now - hit[0]) <= config.PRICE_CACHE_TTL_SECONDS:
            return hit[1]

    resp = requests.get(url, timeout=config.PRICE_HTTP_TIMEOUT)
    resp.raise_for_status()
    data = resp.json()

    with _cache_lock:
        _cache[url] = (now, data)
    return data


# --- symbol normalization / extraction ------------------------------------------

_KNOWN_SYMBOLS = {
    "BTC", "ETH", "USDT", "TRX", "XRP", "DOGE", "LTC", "ADA", "BNB", "SOL",
    "TON", "SHIB", "DOT", "AVAX", "MATIC", "LINK", "ATOM", "USDC", "BCH",
    "ETC", "NOT", "PEPE", "FTM", "NEAR", "APT", "OP", "ARB",
}

# Persian / English display names that should resolve to a symbol above.
_RAW_ALIASES = {
    "بیت کوین": "BTC", "بیتکوین": "BTC", "بیت‌کوین": "BTC", "BITCOIN": "BTC",
    "اتریوم": "ETH", "اتر": "ETH", "ETHEREUM": "ETH",
    "تتر": "USDT", "تتِر": "USDT", "TETHER": "USDT",
    "ترون": "TRX", "ترکس": "TRX", "TRON": "TRX",
    "ریپل": "XRP", "RIPPLE": "XRP",
    "دوج": "DOGE", "دوج کوین": "DOGE", "دوجکوین": "DOGE", "DOGECOIN": "DOGE",
    "لایت کوین": "LTC", "لایتکوین": "LTC", "LITECOIN": "LTC",
    "کاردانو": "ADA", "CARDANO": "ADA",
    "بایننس کوین": "BNB", "بایننس‌کوین": "BNB", "BINANCECOIN": "BNB",
    "سولانا": "SOL", "SOLANA": "SOL",
    "تون کوین": "TON", "تون‌کوین": "TON", "تونکوین": "TON", "TONCOIN": "TON",
    "شیبا": "SHIB", "شیبا اینو": "SHIB", "SHIBA": "SHIB",
    "پولکادات": "DOT", "POLKADOT": "DOT",
    "اولانچ": "AVAX", "AVALANCHE": "AVAX",
}

# Words to strip out of a free-form sentence before hunting for a coin name.
_FILLER_WORDS = sorted(
    [
        "قیمت", "نرخ", "چند شده", "چنده", "چند", "شده", "رو بده", "رو", "بده",
        "میشه", "میباشد", "است", "هست", "لطفا", "لطفاً", "پلیز", "امروز",
        "الان", "حالا",
    ],
    key=len,
    reverse=True,
)

_TICKER_SHAPE = re.compile(r"^[A-Z0-9]{2,10}$")

# Persian display name shown in the price header — falls back to the raw
# symbol for anything not listed here.
_DISPLAY_NAMES = {
    "BTC": "بیت‌کوین",
    "ETH": "اتریوم",
    "USDT": "تتر",
    "TRX": "ترون",
    "XRP": "ریپل",
    "DOGE": "دوج‌کوین",
    "LTC": "لایت‌کوین",
    "ADA": "کاردانو",
    "BNB": "بایننس کوین",
    "SOL": "سولانا",
    "TON": "تون‌کوین",
    "SHIB": "شیبا اینو",
    "DOT": "پولکادات",
    "AVAX": "اولانچ",
    "MATIC": "پالیگان",
    "LINK": "چین‌لینک",
    "ATOM": "کازماس",
    "USDC": "یو‌اس‌دی‌کوین",
    "BCH": "بیت‌کوین‌کش",
    "ETC": "اتریوم کلاسیک",
    "NOT": "نات‌کوین",
    "PEPE": "پپه",
    "FTM": "فانتوم",
    "NEAR": "نیر",
    "APT": "اپتوس",
    "OP": "اپتیمیزم",
    "ARB": "آربیتروم",
}


def display_name(symbol: str) -> str:
    """Persian display name for a symbol — falls back to the symbol itself."""
    return _DISPLAY_NAMES.get(symbol, symbol)


def _gregorian_to_jalali(gy: int, gm: int, gd: int):
    """Convert a Gregorian date to the Jalali (Shamsi) calendar."""
    g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
    if gy > 1600:
        jy = 979
        gy -= 1600
    else:
        jy = 0
        gy -= 621
    gy2 = gy + 1 if gm > 2 else gy
    days = (
        365 * gy
        + (gy2 + 3) // 4
        - (gy2 + 99) // 100
        + (gy2 + 399) // 400
        - 80
        + gd
        + g_d_m[gm - 1]
    )
    jy += 33 * (days // 12053)
    days %= 12053
    jy += 4 * (days // 1461)
    days %= 1461
    if days > 365:
        jy += (days - 1) // 365
        days = (days - 1) % 365
    if days < 186:
        jm = 1 + days // 31
        jd = 1 + (days % 31)
    else:
        jm = 7 + (days - 186) // 30
        jd = 1 + ((days - 186) % 30)
    return jy, jm, jd


def _tehran_now_jalali() -> str:
    """'YYYY/MM/DD | HH:MM:SS' for the current time in Tehran, Jalali calendar."""
    now = datetime.now(timezone.utc) + _TEHRAN_OFFSET
    jy, jm, jd = _gregorian_to_jalali(now.year, now.month, now.day)
    return f"{jy:04d}/{jm:02d}/{jd:02d} | {now.strftime('%H:%M:%S')}"


def normalize_symbol(text) -> str:
    """Upper-case, whitespace/punctuation-free canonical form."""
    s = "" if text is None else str(text)
    s = s.strip().upper()
    s = re.sub(r"[\s_\-]+", "", s)
    s = re.sub(r"[^A-Z0-9؀-ۿ]", "", s)
    return s


def _build_alias_table() -> dict:
    table = {normalize_symbol(sym): sym for sym in _KNOWN_SYMBOLS}
    for alias, symbol in _RAW_ALIASES.items():
        table[normalize_symbol(alias)] = symbol
    return table


_ALIAS_TABLE = _build_alias_table()


def _dynamic_known_symbols() -> set:
    """Best-effort extra symbols pulled from the (cached) Geevex coin list."""
    try:
        data = _cached_get(config.GEEVEX_PRICE_API_URL)
    except Exception:
        return set()
    if not isinstance(data, list):
        return set()
    return {
        normalize_symbol(row.get("symbol"))
        for row in data
        if isinstance(row, dict) and row.get("symbol")
    }


def _lookup(token: str):
    norm = normalize_symbol(token)
    if not norm:
        return None
    if norm in _ALIAS_TABLE:
        return _ALIAS_TABLE[norm]
    # Only pay for a network round-trip when the token is actually
    # ticker-shaped (ASCII letters/digits) — plain Persian chatter never
    # reaches this branch, so idle groups don't generate API traffic.
    if _TICKER_SHAPE.match(norm) and norm in _dynamic_known_symbols():
        return norm
    return None


def _strip_fillers(text: str) -> str:
    out = text
    for word in _FILLER_WORDS:
        out = out.replace(word, " ")
    return out


_PERSIAN_DIGITS = "۰۱۲۳۴۵۶۷۸۹"
_ARABIC_DIGITS = "٠١٢٣٤٥٦٧٨٩"
_DIGIT_MAP = {
    **{d: str(i) for i, d in enumerate(_PERSIAN_DIGITS)},
    **{d: str(i) for i, d in enumerate(_ARABIC_DIGITS)},
    "٫": ".",  # Arabic decimal separator
}

_AMOUNT_RE = re.compile(r"\d+(?:\.\d+)?")


def _normalize_digits(text: str) -> str:
    return "".join(_DIGIT_MAP.get(ch, ch) for ch in text)


def extract_amount(text):
    """
    Find the first number in free-form text (e.g. "1.4 اتریوم میخوام" -> 1.4).
    Accepts Persian/Arabic digits. Returns None when no number is present —
    callers must treat that as "plain price request, no quantity".
    """
    if not text:
        return None
    match = _AMOUNT_RE.search(_normalize_digits(text))
    if not match:
        return None
    try:
        return float(match.group(0))
    except ValueError:
        return None


def extract_symbol(text):
    """
    Find a single recognizable coin symbol inside free-form text.
    Returns the canonical symbol (e.g. "BTC") or None when nothing matched —
    callers must treat None as "not a price request, do nothing".
    """
    if not text:
        return None

    direct = _lookup(text)
    if direct:
        return direct

    cleaned = _strip_fillers(text)

    cleaned_whole = _lookup(cleaned)
    if cleaned_whole:
        return cleaned_whole

    for token in re.split(r"[\s,،.!؟?/:؛]+", cleaned):
        token = token.strip()
        if not token:
            continue
        match = _lookup(token)
        if match:
            return match

    return None


# --- per-exchange fetchers -------------------------------------------------------

def _nobitex_price(symbol: str):
    data = _cached_get(config.NOBITEX_ORDERBOOK_URL)
    if not isinstance(data, dict):
        return None

    irt_row = data.get(f"{symbol}IRT")
    irt_price = irt_row.get("lastTradePrice") if isinstance(irt_row, dict) else None

    if symbol == "USDT":
        usdt_price = "1"
        usdt_row = None
    else:
        usdt_row = data.get(f"{symbol}USDT")
        usdt_price = usdt_row.get("lastTradePrice") if isinstance(usdt_row, dict) else None

    if irt_price is None and usdt_price is None:
        return None

    last_update = None
    for row in (irt_row, usdt_row):
        if isinstance(row, dict) and row.get("lastUpdate"):
            last_update = row.get("lastUpdate")
            break

    return {"irt": irt_price, "usdt": usdt_price, "last_update": last_update}


def _bitpin_price(symbol: str):
    data = _cached_get(config.BITPIN_TICKERS_URL)
    if not isinstance(data, list):
        return None

    irt_price = None
    usdt_price = "1" if symbol == "USDT" else None

    for row in data:
        if not isinstance(row, dict):
            continue
        sym = row.get("symbol")
        if sym == f"{symbol}_IRT":
            irt_price = row.get("price")
        elif symbol != "USDT" and sym == f"{symbol}_USDT":
            usdt_price = row.get("price")

    if irt_price is None and usdt_price is None:
        return None

    return {"irt": irt_price, "usdt": usdt_price, "last_update": None}


def _geevex_price(symbol: str):
    data = _cached_get(config.GEEVEX_PRICE_API_URL)
    if not isinstance(data, list):
        return None

    for row in data:
        if isinstance(row, dict) and row.get("symbol") == symbol:
            return {
                "irt": row.get("irr_buy_price"),
                "usdt": row.get("usdt_buy_price"),
                "last_update": None,
            }
    return None


_SOURCES = (
    ("geevex", _geevex_price, "🔵", "گیوکس"),
    ("nobitex", _nobitex_price, "🟢", "نوبیتکس"),
    ("bitpin", _bitpin_price, "🟡", "بیت‌پین"),
)


def fetch_all(symbol: str) -> dict:
    """
    {'nobitex': {...}|None, 'bitpin': {...}|None, 'geevex': {...}|None}.
    None means that source errored out or doesn't list the coin — the caller
    simply omits that section so the other sources still get shown.
    """
    results = {}
    for name, fetch, _emoji, _label in _SOURCES:
        try:
            results[name] = fetch(symbol)
        except Exception:
            results[name] = None
    return results


def _fmt_number(value):
    if value is None:
        return None
    try:
        num = round(float(value), 2)
    except (TypeError, ValueError):
        return str(value)
    if num == int(num):
        return f"{int(num):,}"
    return f"{num:,.2f}"


def _fmt_amount(value) -> str:
    """Like _fmt_number, but keeps full precision (e.g. 1.4 -> "1.4", not "1.40")."""
    num = float(value)
    if num == int(num):
        return f"{int(num):,}"
    return f"{num:,.8f}".rstrip("0").rstrip(".")


def build_amount_message(amount: float, symbol: str):
    """
    Render "how much is X units of <coin> worth" (Telegram legacy-Markdown,
    all figures in backticks so they're tap-to-copy). Uses the first source
    in _SOURCES priority order (geevex, nobitex, bitpin) that has a price for
    this coin. Returns None if no source has it.
    """
    prices = fetch_all(symbol)
    info = None
    for name, _fetch, _emoji, _label in _SOURCES:
        candidate = prices.get(name)
        if candidate and (candidate.get("usdt") is not None or candidate.get("irt") is not None):
            info = candidate
            break
    if info is None:
        return None

    unit_usdt = info.get("usdt")
    unit_irt = info.get("irt")
    display_name = _DISPLAY_NAMES.get(symbol, symbol)

    header = f"📊 *محاسبه مقدار درخواستی {display_name} ({symbol})*"
    amount_line = f"🔹 *مقدار {display_name}:* {_fmt_amount(amount)} {symbol}"

    price_lines = []
    if unit_usdt is not None:
        price_lines.append(f"💲 *قیمت به تتر:* {_fmt_number(unit_usdt)}")
    if unit_irt is not None:
        price_lines.append(f"💵 *قیمت به ریال:* {_fmt_number(unit_irt)}")

    total_lines = []
    if unit_usdt is not None:
        total_lines.append(f"💲 *ارزش تتر:* {_fmt_number(float(unit_usdt) * amount)}")
    if unit_irt is not None:
        total_lines.append(f"💵 *ارزش ریال:* {_fmt_number(float(unit_irt) * amount)}")

    return "\n\n".join(
        [header, amount_line, "\n".join(price_lines), "\n".join(total_lines)]
    )


def build_price_message(symbol: str) -> str:
    """
    Render the combined price message (Bale legacy-Markdown formatted).
    Same format everywhere — private chat and group replies alike. Returns
    None if every source failed.
    """
    prices = fetch_all(symbol)
    sections = []

    for name, _fetch, emoji, label in _SOURCES:
        info = prices.get(name)
        if not info:
            continue

        irt = _fmt_number(info.get("irt"))
        usdt = _fmt_number(info.get("usdt"))
        if irt is None and usdt is None:
            continue

        lines = [f"{emoji} *{label}*"]
        if usdt is not None:
            lines.append(f"💲 *قیمت به تتر:* {usdt}")
        if irt is not None:
            lines.append(f"💵 *قیمت به ریال:* {irt}")
        sections.append("\n".join(lines))

    if not sections:
        return None

    display_name = _DISPLAY_NAMES.get(symbol, symbol)
    header = f"📊 *قیمت لحظه‌ای {display_name} ({symbol})*"
    body = "\n\n".join(sections)
    footer = f"🕒 *آخرین بروزرسانی:*\n{_tehran_now_jalali()}"

    return f"{header}\n\n{body}\n\n{footer}"
