import json
import os
import re
import secrets
import unicodedata
from datetime import datetime
from decimal import Decimal, InvalidOperation
from zoneinfo import ZoneInfo


PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
CACHE_DIR = os.path.join(PROJECT_DIR, "modules", "cache")
PRICE_LIST_FILE = os.path.join(CACHE_DIR, "price_list.json")
PROMOTION_FILE = os.path.join(CACHE_DIR, "km.json")
SHOP_CONFIG_FILE = os.path.join(CACHE_DIR, "shop_config.json")
SETTINGS_FILE = os.path.join(PROJECT_DIR, "seting.json")
VIETNAM_TIMEZONE = ZoneInfo("Asia/Ho_Chi_Minh")
TRANSFER_REFERENCE_PATTERN = re.compile(r"\bMT\d{12}[A-F0-9]{6}\b")


def load_json_file(file_path, default):
    try:
        with open(file_path, "r", encoding="utf-8") as file:
            return json.load(file)
    except (OSError, json.JSONDecodeError, TypeError):
        return default


def load_catalog():
    catalog = load_json_file(PRICE_LIST_FILE, {})
    return catalog if isinstance(catalog, dict) else {}


def load_shop_config():
    config = load_json_file(SHOP_CONFIG_FILE, {})
    return config if isinstance(config, dict) else {}


def normalize_lookup(value):
    normalized = str(value or "").strip().casefold()
    normalized = re.sub(r"[_-]+", " ", normalized)
    return " ".join(normalized.split())


def compact_lookup(value):
    return normalize_lookup(value).replace(" ", "")


def resolve_product(query, catalog=None):
    catalog = catalog if isinstance(catalog, dict) else load_catalog()
    normalized_query = normalize_lookup(query)
    compact_query = compact_lookup(query)
    if not normalized_query:
        return None

    for product_code, product in catalog.items():
        if normalize_lookup(product_code) == normalized_query:
            return product_code, product

    compact_matches = [
        (product_code, product)
        for product_code, product in catalog.items()
        if compact_lookup(product_code) == compact_query
    ]
    return compact_matches[0] if len(compact_matches) == 1 else None


def resolve_product_selection(query, catalog=None):
    catalog = catalog if isinstance(catalog, dict) else load_catalog()
    direct_match = resolve_product(query, catalog)
    if direct_match:
        product_code, product = direct_match
        return product_code, product, None

    product_query, separator, package_number = str(query or "").strip().rpartition(" ")
    if not separator or not product_query or not package_number:
        return None

    product_match = resolve_product(product_query, catalog)
    if not product_match:
        return None

    product_code, product = product_match
    return product_code, product, package_number.strip()


def find_package(product, package_number):
    expected_number = str(package_number or "").strip().casefold()
    for package in product.get("packages", []):
        if str(package.get("number", "")).strip().casefold() == expected_number:
            return package
    return None


def package_status(package):
    raw_status = normalize_lookup(_ascii_text(package.get("status", "")))
    if raw_status in {"paused", "suspended", "unavailable", "out of stock", "disabled", "tam ngung"}:
        return "paused"
    if raw_status in {"available", "active", "selling", "in stock", "dang ban"}:
        return "available"

    content = str(package.get("content", "")).upper()
    if "TẠM NGƯNG" in content or "TAM NGUNG" in content or "⛔" in content:
        return "paused"
    return "available"


def package_price(package):
    price = package.get("price")
    if isinstance(price, bool):
        return None
    if isinstance(price, int) and price > 0:
        return price
    if isinstance(price, float) and price > 0 and price.is_integer():
        return int(price)
    if isinstance(price, str) and price.strip().isdigit():
        parsed_price = int(price.strip())
        return parsed_price if parsed_price > 0 else None
    return None


def format_vnd(amount):
    return f"{int(amount):,}".replace(",", ".") + "đ"


def format_package_line(package):
    number = str(package.get("number", "?")).strip() or "?"
    content = str(package.get("content") or package.get("title") or "Chưa có thông tin").strip()
    content = re.sub(r"^(?:👍|✅|⛔️?|🟢|🔴)\s*", "", content)
    content = re.sub(
        r"^[\[(]?\s*T(?:Ạ|A)M NG(?:Ư|U)NG\s*[\])]?\s*[:\-–—]?\s*",
        "",
        content,
        flags=re.IGNORECASE,
    )
    if package_status(package) == "paused":
        return f"⛔ Gói {number} [TẠM NGƯNG]: {content}"
    return f"✅ Gói {number}: {content}"


def sorted_packages(product):
    packages = product.get("packages", [])
    if not isinstance(packages, list):
        return []
    return sorted(packages, key=lambda package: package_status(package) == "paused")


def parse_amount(raw_amount):
    value = str(raw_amount or "").strip().casefold()
    value = value.replace("₫", "").replace("đ", "").replace("vnd", "").strip()
    if not value:
        raise ValueError("missing amount")

    suffix_match = re.search(r"(k|nghin|ngan|tr|trieu|m)$", _ascii_text(value))
    if suffix_match:
        suffix = suffix_match.group(1)
        multiplier = 1_000 if suffix in {"k", "nghin", "ngan"} else 1_000_000
        numeric_text = _ascii_text(value)[: -len(suffix)].strip().replace(",", ".")
        if "." in numeric_text and len(numeric_text.rsplit(".", 1)[1]) >= 3:
            raise ValueError("ambiguous amount")
        if not re.fullmatch(r"\d+(?:\.\d+)?", numeric_text):
            raise ValueError("invalid amount")
        try:
            amount_decimal = Decimal(numeric_text) * multiplier
        except InvalidOperation as error:
            raise ValueError("invalid amount") from error
        if amount_decimal != amount_decimal.to_integral_value():
            raise ValueError("invalid amount")
        amount = int(amount_decimal)
    else:
        if value.isdigit():
            numeric_text = value
        elif re.fullmatch(r"\d{1,3}(?:[.,]\d{3})+", value):
            numeric_text = re.sub(r"[.,]", "", value)
        else:
            raise ValueError("invalid amount")
        amount = int(numeric_text)

    if amount <= 0:
        raise ValueError("invalid amount")
    return amount


def validate_amount(amount, payment_config):
    minimum = int(payment_config.get("min_amount", 1_000))
    maximum = int(payment_config.get("max_amount", 500_000_000))
    if amount < minimum or amount > maximum:
        raise ValueError(f"amount must be between {minimum} and {maximum}")
    return amount


def sanitize_transfer_note(note, default_note="THANH TOAN MORETECH", max_length=50):
    normalized = _ascii_text(note or default_note).upper()
    normalized = re.sub(r"[^A-Z0-9 _.-]", " ", normalized)
    normalized = " ".join(normalized.split())
    return (normalized or default_note)[:max_length].strip()


def prepare_transfer_note(note, payment_config):
    default_note = payment_config.get("default_note", "THANH TOAN MORETECH")
    max_length = int(payment_config.get("max_note_length", 50))
    reference = "MT" + datetime.now(VIETNAM_TIMEZONE).strftime("%y%m%d%H%M%S") + secrets.token_hex(3).upper()
    if max_length < len(reference):
        raise ValueError("max_note_length is too short for payment reference")

    sanitized_note = sanitize_transfer_note(note, default_note=default_note, max_length=max_length)
    sanitized_note = TRANSFER_REFERENCE_PATTERN.sub("", sanitized_note)
    sanitized_note = " ".join(sanitized_note.split())
    available_length = max_length - len(reference) - 1
    base_note = sanitized_note[:max(0, available_length)].rstrip()
    return f"{base_note} {reference}".strip()


def validate_prepared_transfer_note(note, payment_config):
    max_length = int(payment_config.get("max_note_length", 50))
    sanitized_note = sanitize_transfer_note(note, default_note="", max_length=max_length)
    references = TRANSFER_REFERENCE_PATTERN.findall(sanitized_note)
    if len(references) != 1 or not sanitized_note.endswith(references[0]):
        raise ValueError("invalid prepared payment reference")
    return sanitized_note


def get_active_promotions(product_code=None, current_date=None):
    promotions = load_json_file(PROMOTION_FILE, [])
    if not isinstance(promotions, list):
        return []

    today = current_date or datetime.now(VIETNAM_TIMEZONE).date()
    if isinstance(today, datetime):
        today = today.date()
    active_promotions = []
    for promotion in promotions:
        if not isinstance(promotion, dict):
            continue
        expiry_date = _parse_vietnamese_date(promotion.get("expiry_date"))
        start_date_value = promotion.get("start_date")
        start_date = _parse_vietnamese_date(start_date_value)
        if not expiry_date or expiry_date < today:
            continue
        if start_date_value not in (None, "") and not start_date:
            continue
        if start_date and start_date > today:
            continue
        if product_code and not _promotion_applies_to_product(promotion, product_code):
            continue
        active_promotions.append((expiry_date, promotion))

    active_promotions.sort(key=lambda item: item[0])
    return [promotion for _, promotion in active_promotions]


def get_sales_user_ids():
    settings = load_json_file(SETTINGS_FILE, {})
    shop_config = load_shop_config()
    user_ids = set()
    for key in ("admin", "adm", "sale", "sales"):
        _collect_user_ids(user_ids, settings.get(key))
    _collect_user_ids(user_ids, shop_config.get("sales_ids"))
    return user_ids


def is_sales_user(author_id):
    return str(author_id) in get_sales_user_ids()


def current_prefix(client, fallback="."):
    command_handler = getattr(client, "command_handler", None)
    return getattr(command_handler, "current_prefix", None) or fallback


def command_aliases(client, command):
    command_handler = getattr(client, "command_handler", None)
    aliases = getattr(command_handler, "alias_commands", {}) or {}
    command_alias_list = aliases.get(command, []) if isinstance(aliases, dict) else []
    return command_alias_list if isinstance(command_alias_list, list) else []


def extract_command_args(message, command, prefix=".", allow_compact=False, aliases=None):
    text = str(message or "").strip()
    for candidate in [command, *(aliases or [])]:
        expected = f"{prefix}{candidate}"
        if text.casefold() == expected.casefold():
            return ""
        if not text.casefold().startswith(expected.casefold()):
            continue

        remainder = text[len(expected):]
        if remainder and remainder[0].isspace():
            return remainder.strip()
        if allow_compact and candidate == command and remainder:
            return remainder.strip()
    return None


def resolve_command_name(message_text, prefix, commands, aliases=None, rsearch_commands=None, compact_commands=None):
    text = str(message_text or "").strip()
    if not prefix or not text.casefold().startswith(str(prefix).casefold()):
        return None

    command_part = text[len(str(prefix)):].strip()
    if not command_part:
        return None
    command_token = command_part.split(maxsplit=1)[0].casefold()

    canonical_commands = {str(command).casefold(): command for command in commands}
    if command_token in canonical_commands:
        return canonical_commands[command_token]

    for command, command_aliases in (aliases or {}).items():
        if command not in commands:
            continue
        if any(command_token == str(alias).casefold() for alias in command_aliases):
            return command

    for command in rsearch_commands or []:
        if command in commands and command_token == str(command).casefold():
            return command

    for command in compact_commands or []:
        normalized_command = str(command).casefold()
        if command in commands and command_token.startswith(normalized_command) and len(command_token) > len(normalized_command):
            return command
    return None


def split_text(text, limit=3000):
    if len(text) <= limit:
        return [text]

    chunks = []
    current_lines = []
    current_length = 0
    for line in text.splitlines(keepends=True):
        if current_lines and current_length + len(line) > limit:
            chunks.append("".join(current_lines).rstrip())
            current_lines = []
            current_length = 0
        while len(line) > limit:
            chunks.append(line[:limit].rstrip())
            line = line[limit:]
        current_lines.append(line)
        current_length += len(line)
    if current_lines:
        chunks.append("".join(current_lines).rstrip())
    return [chunk for chunk in chunks if chunk]


def _ascii_text(value):
    text = str(value or "").replace("Đ", "D").replace("đ", "d")
    return "".join(
        character
        for character in unicodedata.normalize("NFD", text)
        if unicodedata.category(character) != "Mn"
    )


def _parse_vietnamese_date(value):
    try:
        return datetime.strptime(str(value), "%d/%m/%Y").date()
    except (TypeError, ValueError):
        return None


def _promotion_applies_to_product(promotion, product_code):
    targets = promotion.get("product_codes")
    if targets is None:
        targets = promotion.get("products", promotion.get("product_code"))
    if not targets:
        return bool(promotion.get("all_products") or promotion.get("scope") == "all")
    if isinstance(targets, str):
        targets = [targets]
    if not isinstance(targets, list):
        return False
    expected = normalize_lookup(product_code)
    return any(normalize_lookup(target) == expected for target in targets)


def _collect_user_ids(destination, values):
    if values is None:
        return
    if not isinstance(values, (list, tuple, set)):
        values = [values]
    for value in values:
        if value is not None and str(value).strip():
            destination.add(str(value))


def save_json_file(file_path, data, indent=4):
    with open(file_path, "w", encoding="utf-8") as file:
        json.dump(data, file, indent=indent, ensure_ascii=False)


# Giá hiện chỉ nằm trong chuỗi mô tả ("520k/tháng"), không có trường `price`.
# Rút số ra để Sale copy lệnh .stk có sẵn số tiền, nhưng KHÔNG tự tạo QR:
# tiền của khách thì con người vẫn phải xác nhận.
MIN_PRICE_IN_TEXT = 10_000
PRICE_IN_TEXT_PATTERN = re.compile(
    r"(?<![\d.,])(\d{1,3}(?:[.,]\d{3})+|\d+(?:[.,]\d+)?\s*(?:k|tr|triệu|trieu)|\d{4,9})(?![\d])",
    re.IGNORECASE,
)


def price_from_content(content):
    """Đoán giá từ mô tả gói. Trả None khi không chắc chắn."""
    text = str(content or "")
    for match in PRICE_IN_TEXT_PATTERN.finditer(text):
        candidate = match.group(1).replace(" ", "")
        try:
            amount = parse_amount(candidate)
        except (TypeError, ValueError):
            continue
        # Sàn 10.000đ: chặn "Netflix 4K" -> 4.000đ, "2K" -> 2.000đ.
        # Gói rẻ nhất trong bảng giá hiện tại là 190k nên sàn này không cắt nhầm.
        if amount >= MIN_PRICE_IN_TEXT:
            return amount
    return None


def package_price_hint(package):
    """Giá chắc chắn (trường price) nếu có, nếu không thì giá đoán từ mô tả."""
    exact = package_price(package)
    if exact:
        return exact, True
    return price_from_content(package.get("content") or package.get("title")), False


if __name__ == "__main__":
    # self-check cho phần đoán giá: sai số tiền là sai tiền của khách
    CASES = [
        ("🇻🇳 ChatGPT Business – chính chủ: 520k/tháng", 520_000),
        ("Tài khoản cấp sẵn - DÙNG CHUNG: 190k", 190_000),
        ("ChatGPT Go – cấp sẵn: 350k/năm", 350_000),
        ("Gói 1 tháng giá 1.200.000", 1_200_000),
        # "1tr5" không có trong bảng giá hiện tại -> trả None, Sale gõ tay (an toàn hơn đoán)
        ("Gói 3 tháng 1tr5", None),
        # Những chuỗi KHÔNG phải giá — đoán bừa là gửi nhầm số tiền cho khách
        ("Netflix Premium 4K 1 thiết bị", None),
        ("Màn hình 2K sắc nét", None),
        ("Google AI Pro - 2TB", None),
        ("Gói 2 người dùng", None),
        ("Nâng cấp 12 tháng", None),
        ("Zoom Pro 100 người", None),
        ("", None),
        (None, None),
    ]
    for content, expected in CASES:
        actual = price_from_content(content)
        assert actual == expected, f"{content!r}: {actual} != {expected}"

    assert package_price_hint({"price": 99_000, "content": "abc 520k"}) == (99_000, True)
    assert package_price_hint({"content": "gói xịn 520k/tháng"}) == (520_000, False)
    assert package_price_hint({"content": "Netflix 4K"}) == (None, False)
    print("sales_utils price self-check OK")
