"""Stock tool for fetching stock market information."""

import json
import os
import re

_STOCK_STOPWORDS = {
    "STOCK",
    "STOCKS",
    "SAHAM",
    "PRICE",
    "HARGA",
    "MARKET",
    "PASAR",
    "NOW",
    "SEKARANG",
    "TODAY",
    "HARI",
    "INI",
    "BERAPA",
    "CEK",
    "CHECK",
    "TOLONG",
    "PLEASE",
    "KALAU",
    "COBA",
    "DAN",
    "ATAU",
    "BANK",
    "INDONESIA",
    "IDX",
    "IHSG",
    "BEI",
    "JAKARTA",
    "IYA",
    "YA",
    "GAS",
    "LANJUT",
    "OK",
    "OKE",
    "MAAF",
    "KAMU",
    "AKU",
    "BENER",
    "BENAR",
    "BISA",
    "BISAKAH",
}

_BASE_IDX_ALIAS_TO_SYMBOL = {
    "BBCA": "BBCA.JK",
    "BCA": "BBCA.JK",
    "BANK CENTRAL ASIA": "BBCA.JK",
    "BBRI": "BBRI.JK",
    "BRI": "BBRI.JK",
    "BANK RAKYAT INDONESIA": "BBRI.JK",
    "BMRI": "BMRI.JK",
    "MANDIRI": "BMRI.JK",
    "BANK MANDIRI": "BMRI.JK",
    "BBNI": "BBNI.JK",
    "BNI": "BBNI.JK",
    "BANK NEGARA INDONESIA": "BBNI.JK",
    "TLKM": "TLKM.JK",
    "TELKOM": "TLKM.JK",
    "ASII": "ASII.JK",
    "GOTO": "GOTO.JK",
    "ANTM": "ANTM.JK",
    "UNVR": "UNVR.JK",
    "INDF": "INDF.JK",
    "ICBP": "ICBP.JK",
    "ADARO": "ADRO.JK",
    "ADARO ENERGY": "ADRO.JK",
    "ADARO ENERGY INDONESIA": "ADRO.JK",
    "TOBA": "TOBA.JK",
    "TOBA BARA": "TOBA.JK",
    "TOBA BARA SEJAHTRA": "TOBA.JK",
    # FX aliases (Yahoo Finance format)
    "USDIDR": "USDIDR=X",
    "USD IDR": "USDIDR=X",
    "USD TO IDR": "USDIDR=X",
    "USD KE IDR": "USDIDR=X",
    "USD KE RUPIAH": "USDIDR=X",
    "KURS USD": "USDIDR=X",
    "KURS DOLLAR": "USDIDR=X",
    "DOLLAR KE RUPIAH": "USDIDR=X",
    "1 USD BERAPA RUPIAH": "USDIDR=X",
    "US DOLLAR TO RUPIAH": "USDIDR=X",
}

_KNOWN_IDX_SYMBOLS = {
    "BBCA",
    "BBRI",
    "BMRI",
    "BBNI",
    "TLKM",
    "ASII",
    "GOTO",
    "ANTM",
    "UNVR",
    "INDF",
    "ICBP",
    "ADRO",
    "TOBA",
    "MDKA",
    "PTBA",
    "ACES",
    "SMGR",
    "EXCL",
    "ISAT",
    "KLBF",
    "SIDO",
    "CPIN",
    "JPFA",
    "AMRT",
    "MAPI",
    "SRTG",
    "TOWR",
}

_KNOWN_GLOBAL_SYMBOLS = {
    "AAPL",
    "MSFT",
    "TSLA",
    "NVDA",
    "META",
    "GOOG",
    "AMZN",
}

_PLACEHOLDER_ROOTS = {"XXXX", "TICKER", "SYMBOL", "EXAMPLE"}
_EXPLICIT_SYMBOL_RE = re.compile(r"^[A-Z0-9]{1,8}\.[A-Z]{1,4}$")
_FX_SYMBOL_RE = re.compile(r"^[A-Z]{3,10}=X$")
_BARE_SYMBOL_RE = re.compile(r"^[A-Z]{1,5}$")
_GENERAL_SYMBOL_RE = re.compile(r"^[A-Z0-9][A-Z0-9.\-^=]{0,14}$")
_COIN_ID_RE = re.compile(r"^[a-z0-9-]{2,64}$")
_ALIAS_KEY_RE = re.compile(r"[^a-z0-9]+")
_NON_TICKER_FILE_SUFFIXES = {
    "JSON",
    "YAML",
    "YML",
    "TOML",
    "INI",
    "CONF",
    "CFG",
    "TXT",
    "MD",
    "CSV",
    "PDF",
    "DOC",
    "DOCX",
    "XML",
    "HTML",
    "HTM",
    "JS",
    "TS",
    "PY",
    "GO",
    "JAVA",
    "C",
    "CPP",
    "H",
    "HPP",
    "SH",
    "BAT",
    "PS1",
    "LOG",
    "DB",
}
_STOCK_ALIAS_ENV_PATH = "KABOT_STOCK_ALIASES_PATH"
_STOCK_ALIAS_DEFAULT_PATH = os.path.expanduser("~/.kabot/stock_aliases.json")
_YAHOO_SEARCH_URLS = (
    "https://query2.finance.yahoo.com/v1/finance/search",
    "https://query1.finance.yahoo.com/v1/finance/search",
)
_YAHOO_AUTOC_URL = "https://autoc.finance.yahoo.com/autoc"
_YAHOO_SEARCH_ACCEPTED_QUOTE_TYPES = {"EQUITY", "ETF", "MUTUALFUND", "INDEX"}
_STOCK_SEARCH_QUERY_TIMEOUT_SECONDS = 6.0
_STOCK_SEARCH_MAX_COMPANY_CANDIDATES = 3
_STOCK_SEARCH_QUOTES_PER_QUERY = 8
_STOCK_SEARCH_SYMBOLS_PER_QUERY = 2
_STOCK_RESOLVE_CACHE_TTL_SECONDS = 600
_STOCK_RESOLVE_CACHE_MAX_ITEMS = 256
_SECONDARY_LISTING_MARKERS = (
    " adr",
    " ads",
    " cdr",
    " hedged",
    " depositary",
    " depository",
)
_MARKET_HINT_SUFFIXES: tuple[tuple[str, tuple[str, ...]], ...] = (
    ("jepang", (".T",)),
    ("japan", (".T",)),
    ("tokyo", (".T",)),
    ("indonesia", (".JK",)),
    ("idx", (".JK",)),
    ("jakarta", (".JK",)),
    ("germany", (".DE",)),
    ("jerman", (".DE",)),
    ("deutschland", (".DE",)),
    ("canada", (".TO",)),
    ("toronto", (".TO",)),
    ("uk", (".L",)),
    ("london", (".L",)),
    ("thailand", (".BK",)),
    ("bangkok", (".BK",)),
    ("philippines", (".PS",)),
    ("filipina", (".PS",)),
    ("manila", (".PS",)),
    ("hong kong", (".HK",)),
    ("china", (".HK", ".SS", ".SZ")),
    ("india", (".NS", ".BO")),
    ("us", ("",)),
    ("usa", ("",)),
    ("nyse", ("",)),
    ("nasdaq", ("",)),
)

_STOCK_NAME_STOPWORDS = {
    *(word.lower() for word in _STOCK_STOPWORDS),
    "the",
    "a",
    "an",
    "this",
    "that",
    "is",
    "are",
    "to",
    "for",
    "with",
    "at",
    "on",
    "of",
    "from",
    "dengan",
    "apa",
    "nya",
    "bagus",
    "dari",
    "yang",
    "itu",
    "ini",
    "saya",
    "anda",
    "kita",
    "kami",
    "bro",
    "sis",
    "broh",
    "cenderung",
    "naik",
    "turun",
    "trend",
    "trending",
    "movement",
    "pergerakan",
    "bullish",
    "bearish",
    "sideways",
    "dirupiahkan",
    "rupiahkan",
    "if",
    "around",
    "about",
    "roughly",
    "approximately",
    "approx",
    "kira-kira",
    "dollar",
    "dollars",
    "usd",
    "idr",
    "rupiah",
    "indonesian",
    "today",
    "current",
    "worth",
    "convert",
    "converted",
    "conversion",
    "much",
    "how",
    "is",
    "that",
    "in",
    "right",
    "currently",
    "latest",
    "umur",
    "age",
    "old",
    "tahun",
    "year",
}
_COMPANY_HINT_TOKENS = {
    "bank",
    "inc",
    "corp",
    "corporation",
    "company",
    "co",
    "ltd",
    "plc",
    "group",
    "holding",
    "holdings",
    "tbk",
    "pt",
    "sa",
    "se",
    "ag",
}
_COMPANY_SPLIT_RE = re.compile(
    r"(?i)[,;]|(?:\b(?:and|dan|atau|or|plus|serta|with|vs)\b)"
)
_COMPANY_TOKEN_RE = re.compile(r"[^\W_][^\s,;:!?]{0,31}", re.UNICODE)
_PERSONAL_CHAT_MARKERS = {
    "umur",
    "age",
    "old",
    "years old",
    "berapa umur",
    "how old",
}
_ADVICE_CHAT_MARKERS_RE = re.compile(
    r"\b(saran(?:mu)?|advice|suggest(?:ion)?|recommend(?:ation)?)\b",
    re.IGNORECASE,
)
_MARKET_CONTEXT_MARKERS_RE = re.compile(
    r"\b(stock|saham|ticker|symbol|quote|market|harga|price|kurs|exchange)\b",
    re.IGNORECASE,
)
_VALUE_QUERY_HINT_RE = re.compile(
    r"\b(price|harga|quote|berapa|how much|rate|kurs)\b",
    re.IGNORECASE,
)
_USD_AMOUNT_RE = re.compile(
    r"(?i)\b(?:around|about|roughly|sekitar|kurang lebih|kira-kira)?\s*(\d+(?:[.,]\d+)?)\s*(usd|dollar|dollars)\b"
)
_USD_MARKER_RE = re.compile(r"(?i)\b(usd|dollar|dollars)\b")
_IDR_MARKER_RE = re.compile(r"(?i)\b(idr|rupiah|indonesian rupiah|dirupiahkan|rupiahkan)\b")
_NON_ASCII_RE = re.compile(r"[^\x00-\x7F]")

_CRYPTO_ALIAS_TO_ID = {
    "BITCOIN": "bitcoin",
    "BTC": "bitcoin",
    "ETHEREUM": "ethereum",
    "ETH": "ethereum",
    "SOLANA": "solana",
    "SOL": "solana",
    "DOGECOIN": "dogecoin",
    "DOGE": "dogecoin",
    "BINANCECOIN": "binancecoin",
    "BNB": "binancecoin",
    "CARDANO": "cardano",
    "ADA": "cardano",
    "RIPPLE": "ripple",
    "XRP": "ripple",
    "LITECOIN": "litecoin",
    "LTC": "litecoin",
}


def _looks_like_free_text(text: str) -> bool:
    tokens = [t for t in re.split(r"\s+", text.strip()) if t]
    if len(tokens) >= 4:
        return True
    return any(ch in text for ch in "?!:;\n")


def _edit_distance_leq_one(left: str, right: str) -> bool:
    if left == right:
        return True
    if abs(len(left) - len(right)) > 1:
        return False
    i = 0
    j = 0
    mismatches = 0
    while i < len(left) and j < len(right):
        if left[i] == right[j]:
            i += 1
            j += 1
            continue
        mismatches += 1
        if mismatches > 1:
            return False
        if len(left) > len(right):
            i += 1
        elif len(left) < len(right):
            j += 1
        else:
            i += 1
            j += 1
    if i < len(left) or j < len(right):
        mismatches += 1
    return mismatches <= 1


def _resolve_idx_typo(symbol: str) -> str | None:
    token = str(symbol or "").upper().strip()
    if not token or len(token) < 3:
        return None
    for known in _KNOWN_IDX_SYMBOLS:
        if _edit_distance_leq_one(token, known):
            return known
    return None


def _normalize_alias_key(value: str) -> str:
    collapsed = _ALIAS_KEY_RE.sub(" ", str(value or "").lower())
    return " ".join(collapsed.split()).strip()


def _normalize_stock_symbol(value: str) -> str | None:
    symbol = str(value or "").upper().strip()
    if not symbol:
        return None
    if _FX_SYMBOL_RE.match(symbol):
        return symbol
    if _EXPLICIT_SYMBOL_RE.match(symbol):
        left, right = symbol.split(".", 1)
        if right == "JK" and left not in _KNOWN_IDX_SYMBOLS:
            corrected = _resolve_idx_typo(left)
            if corrected:
                return f"{corrected}.JK"
        return symbol
    if symbol in _KNOWN_IDX_SYMBOLS:
        return f"{symbol}.JK"
    if symbol in _KNOWN_GLOBAL_SYMBOLS:
        return symbol
    return None


def _normalize_search_symbol(value: str) -> str | None:
    """Normalize Yahoo search symbol into a safe tradable token."""
    normalized = _normalize_stock_symbol(value)
    if normalized:
        return normalized

    symbol = str(value or "").upper().strip()
    if not symbol:
        return None
    if _GENERAL_SYMBOL_RE.match(symbol):
        if symbol.startswith("^"):
            # Skip indices in this stock tool path; they are not equities.
            return None
        return symbol
    return None


def _preferred_market_suffixes(raw_query: str) -> list[str]:
    normalized = " ".join(str(raw_query or "").strip().lower().split())
    if not normalized:
        return []
    out: list[str] = []
    seen: set[str] = set()
    for hint, suffixes in _MARKET_HINT_SUFFIXES:
        if hint in normalized:
            for suffix in suffixes:
                if suffix in seen:
                    continue
                seen.add(suffix)
                out.append(suffix)
    return out


def _rank_symbols_for_query(symbols: list[str], raw_query: str) -> list[str]:
    preferences = _preferred_market_suffixes(raw_query)
    if not preferences:
        return symbols

    def _score(symbol: str) -> int:
        token = str(symbol or "").upper().strip()
        if not token:
            return -1
        for idx, suffix in enumerate(preferences):
            if suffix == "":
                if "." not in token:
                    return 100 - idx
                continue
            if token.endswith(suffix.upper()):
                return 100 - idx
        return 0

    decorated = [(idx, symbol, _score(symbol)) for idx, symbol in enumerate(symbols)]
    decorated.sort(key=lambda item: (-item[2], item[0]))
    return [symbol for _idx, symbol, _score_value in decorated]


def _symbol_market_bucket(symbol: str) -> str:
    token = str(symbol or "").upper().strip()
    if "." in token:
        return token.rsplit(".", 1)[1]
    return "US"


def _looks_like_personal_small_talk(text: str) -> bool:
    normalized = " ".join(str(text or "").strip().lower().split())
    if not normalized:
        return False
    if any(marker in normalized for marker in _PERSONAL_CHAT_MARKERS):
        return True
    if _ADVICE_CHAT_MARKERS_RE.search(normalized) and not _MARKET_CONTEXT_MARKERS_RE.search(normalized):
        return True
    return False


def _is_non_latin_compact_query(text: str) -> bool:
    """Heuristic for short non-Latin company-name style queries."""
    raw = str(text or "").strip()
    if not raw:
        return False
    if not _NON_ASCII_RE.search(raw):
        return False
    if len(raw) > 80:
        return False
    if re.search(r"(https?://|www\.)", raw, re.IGNORECASE):
        return False
    return True


def extract_stock_name_candidates(raw: str) -> list[str]:
    """
    Extract probable company-name phrases from free-text stock queries.

    This is intentionally conservative to avoid converting generic chat text into
    market lookups.
    """
    text = str(raw or "").strip()
    if not text:
        return []
    if _looks_like_personal_small_talk(text):
        return []
    # If explicit symbols already exist, don't trigger name-based fallback.
    if extract_stock_symbols(text):
        return []

    cleaned = re.sub(r"[\(\)\[\]\{\}\"'`]+", " ", text)
    chunks = [part.strip() for part in _COMPANY_SPLIT_RE.split(cleaned) if part.strip()]
    has_market_hint = bool(_MARKET_CONTEXT_MARKERS_RE.search(text))
    has_value_hint = bool(_VALUE_QUERY_HINT_RE.search(text))
    raw_token_count = len([token for token in re.split(r"\s+", cleaned.strip()) if token])

    candidates: list[str] = []
    seen: set[str] = set()
    for chunk in chunks:
        tokens = [match.group(0) for match in _COMPANY_TOKEN_RE.finditer(chunk)]
        kept: list[str] = []
        for token in tokens:
            token_clean = token.strip(".,:;!?")
            if not token_clean:
                continue
            token_lower = token_clean.lower()
            if token_lower in _STOCK_NAME_STOPWORDS:
                continue
            if re.fullmatch(r"\d+(?:\.\d+)?", token_lower):
                continue
            if len(token_lower) <= 1:
                continue
            kept.append(token_clean)

        if not kept:
            continue
        # Prevent long prose from being sent to market symbol search.
        if len(kept) > 5:
            continue

        phrase = " ".join(kept).strip(" .,:;!?-")
        if not phrase:
            continue
        phrase_tokens = [token.lower() for token in re.split(r"\s+", phrase) if token]
        if not phrase_tokens:
            continue
        if (
            len(phrase_tokens) == 1
            and raw_token_count > 2
            and not (has_market_hint or has_value_hint)
        ):
            continue
        if len(phrase_tokens) >= 2:
            has_company_hint = any(token in _COMPANY_HINT_TOKENS for token in phrase_tokens)
            has_title_like = any(bool(token[:1].isupper()) for token in kept if token)
            has_non_latin = bool(_NON_ASCII_RE.search(phrase))
            if not (
                has_market_hint
                or has_value_hint
                or has_company_hint
                or has_title_like
                or has_non_latin
            ):
                continue
        key = phrase.casefold()
        if key in seen:
            continue
        seen.add(key)
        candidates.append(phrase)
        if len(candidates) >= _STOCK_SEARCH_MAX_COMPANY_CANDIDATES:
            break

    if candidates:
        return candidates

    # Non-Latin fallback: keep compact original phrase so global users can query
    # by company name/script without explicit ticker suffix.
    compact = re.sub(r"\s+", " ", cleaned).strip(" .,:;!?-")
    if _is_non_latin_compact_query(compact):
        return [compact]

    return []


def _load_custom_idx_aliases() -> dict[str, str]:
    """Load optional stock aliases from JSON file for skill/user extensibility."""
    alias_path = str(os.environ.get(_STOCK_ALIAS_ENV_PATH, "") or "").strip()
    candidate_paths = [alias_path] if alias_path else []
    candidate_paths.append(_STOCK_ALIAS_DEFAULT_PATH)

    for path in candidate_paths:
        if not path or not os.path.isfile(path):
            continue
        try:
            with open(path, "r", encoding="utf-8") as handle:
                payload = json.load(handle)
        except Exception:
            continue

        source = payload.get("aliases") if isinstance(payload, dict) and isinstance(payload.get("aliases"), dict) else payload
        if not isinstance(source, dict):
            continue

        parsed: dict[str, str] = {}
        for raw_alias, raw_symbol in source.items():
            alias = _normalize_alias_key(str(raw_alias or ""))
            symbol = _normalize_stock_symbol(str(raw_symbol or ""))
            if alias and symbol:
                parsed[alias] = symbol
        if parsed:
            return parsed
    return {}


def _build_idx_alias_map() -> dict[str, str]:
    alias_map: dict[str, str] = {}
    for raw_alias, raw_symbol in _BASE_IDX_ALIAS_TO_SYMBOL.items():
        alias = _normalize_alias_key(raw_alias)
        symbol = _normalize_stock_symbol(raw_symbol)
        if alias and symbol:
            alias_map[alias] = symbol
    alias_map.update(_load_custom_idx_aliases())
    return alias_map


def _extract_alias_symbols_in_order(normalized_text: str, alias_map: dict[str, str]) -> list[str]:
    hits: list[tuple[int, int, str]] = []
    for alias, symbol in alias_map.items():
        if not alias or not symbol:
            continue
        pattern = re.compile(rf"(?<![a-z0-9]){re.escape(alias)}(?![a-z0-9])")
        for match in pattern.finditer(normalized_text):
            hits.append((match.start(), len(alias), symbol))
    hits.sort(key=lambda item: (item[0], -item[1]))

    ordered_symbols: list[str] = []
    seen: set[str] = set()
    for _start, _alias_len, symbol in hits:
        if symbol in seen:
            continue
        seen.add(symbol)
        ordered_symbols.append(symbol)
    return ordered_symbols


def extract_stock_symbols(raw: str) -> list[str]:
    text = (raw or "").strip()
    if not text:
        return []

    free_text = _looks_like_free_text(text)
    raw_tokens = [tok for tok in re.split(r"[,\s]+", text) if tok]
    allow_unknown_bare = len(raw_tokens) == 1 or "," in text
    alias_map = _build_idx_alias_map()
    normalized_alias_text = _normalize_alias_key(text)

    result: list[str] = []
    seen: set[str] = set()

    def _add(symbol: str) -> None:
        normalized = symbol.upper().strip()
        if not normalized or normalized in seen:
            return
        seen.add(normalized)
        result.append(normalized)

    for mapped in _extract_alias_symbols_in_order(normalized_alias_text, alias_map):
        _add(mapped)

    for token_raw in raw_tokens:
        token = token_raw.strip().strip("()[]{}\"'`")
        token = token.strip(".,;:!?")
        if not token:
            continue

        symbol = token.upper()
        if symbol in _STOCK_STOPWORDS:
            continue

        mapped = alias_map.get(_normalize_alias_key(symbol))
        if mapped:
            _add(mapped)
            continue

        if _EXPLICIT_SYMBOL_RE.match(symbol):
            left, right = symbol.split(".", 1)
            if left in _PLACEHOLDER_ROOTS:
                continue
            if right in _NON_TICKER_FILE_SUFFIXES and left not in _KNOWN_IDX_SYMBOLS and left not in _KNOWN_GLOBAL_SYMBOLS:
                continue
            if len(set(left)) == 1 and len(left) >= 3:
                continue
            if right == "JK" and left not in _KNOWN_IDX_SYMBOLS:
                corrected = _resolve_idx_typo(left)
                if corrected:
                    _add(f"{corrected}.JK")
                    continue
            _add(symbol)
            continue

        if _FX_SYMBOL_RE.match(symbol):
            _add(symbol)
            continue

        if symbol in _KNOWN_IDX_SYMBOLS:
            _add(f"{symbol}.JK")
            continue

        if symbol in _KNOWN_GLOBAL_SYMBOLS:
            _add(symbol)
            continue

        if (
            allow_unknown_bare
            and not free_text
            and _BARE_SYMBOL_RE.match(symbol)
            and token == token.upper()
        ):
            _add(symbol)

    return result


# Backward-compat alias for older imports/tests.
def _extract_stock_symbols(raw: str) -> list[str]:
    return extract_stock_symbols(raw)


def extract_crypto_ids(raw: str) -> list[str]:
    text = str(raw or "").strip()
    if not text:
        return []

    tokens = [tok for tok in re.split(r"[,\s]+", text) if tok]
    result: list[str] = []
    seen: set[str] = set()

    def _add(coin_id: str) -> None:
        normalized = str(coin_id or "").strip().lower()
        if not normalized or normalized in seen:
            return
        seen.add(normalized)
        result.append(normalized)

    for token_raw in tokens:
        token = token_raw.strip().strip("()[]{}\"'`")
        token = token.strip(".,;:!?")
        if not token:
            continue

        mapped = _CRYPTO_ALIAS_TO_ID.get(token.upper())
        if mapped:
            _add(mapped)
            continue

        token_lower = token.lower()
        if token_lower in _CRYPTO_ALIAS_TO_ID.values():
            _add(token_lower)

    return result
