#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
AKShare CLI Harness - Main CLI Entry Point

A complete CLI for the akshare financial data library.
Supports one-shot commands, REPL mode, and JSON output.

Usage:
    akshare-cli call stock_zh_a_hist --symbol 000001
    akshare-cli stock hist 000001 --json
    akshare-cli search fund --json
    akshare-cli repl
"""

import functools
import json
import os
import shlex
import sys
import traceback
from typing import Optional

import click
import pandas as pd

from akshare_cli.core.cache import _result_cache
from akshare_cli.core.registry import (
    call_function,
    get_all_functions,
    get_domains,
    get_function,
    get_function_info,
    list_functions_by_domain,
    search_functions,
    set_retry,
)
from akshare_cli.core.docsource import get_function_source
from akshare_cli.core.session import Session
from akshare_cli.core.export import auto_export
from akshare_cli.utils.formatting import (
    display_result,
    print_error,
    print_info,
)

# Global session
_session = Session()


class OutputConfig:
    """Holds output configuration for the current invocation."""

    def __init__(self):
        self.format = "table"
        self.output_file = None
        self.max_rows = None
        self.show_index = False
        self.no_header = False

    def apply(self, use_json=False, use_csv=False, output_file=None, limit=None, no_header=False):
        """Apply output options, only overriding if explicitly set."""
        if use_json:
            self.format = "json"
        if use_csv:
            self.format = "csv"
        if output_file is not None:
            self.output_file = output_file
        if limit is not None:
            self.max_rows = limit
        if no_header:
            self.no_header = no_header


_output_config = OutputConfig()


def output_options(func):
    """Decorator that adds --json/--csv/--output/--limit/--no-header to any command."""
    @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
    @click.option("--csv", "use_csv", is_flag=True, help="Output as CSV.")
    @click.option("--output", "-o", "output_file", default=None, help="Save output to file.")
    @click.option("--limit", "-n", "limit", type=int, default=None, help="Limit number of rows.")
    @click.option("--no-header", "no_header", is_flag=True, help="Suppress table headers.")
    @functools.wraps(func)
    def wrapper(*args, use_json=False, use_csv=False, output_file=None, limit=None, no_header=False, **kwargs):
        _output_config.apply(use_json, use_csv, output_file, limit, no_header)
        return func(*args, **kwargs)
    return wrapper


def _handle_result(result, func_name: str = "", kwargs: dict = None):
    """Process and display a function result."""
    kwargs = kwargs or {}

    if isinstance(result, pd.DataFrame):
        _session.record_call(func_name, kwargs, result)

        if result.empty:
            if _output_config.format == "json":
                click.echo(json.dumps({"total_rows": 0, "columns": [], "data": []}, indent=2))
            else:
                click.echo("No data returned (empty DataFrame).")
            return

        output = display_result(
            result,
            output_format=_output_config.format,
            max_rows=_output_config.max_rows,
            show_index=_output_config.show_index,
            output_file=_output_config.output_file,
        )
        click.echo(output)

        if _output_config.format == "table" and _output_config.output_file is None:
            click.echo(
                f"\n[{result.shape[0]} rows x {result.shape[1]} columns]",
                err=True,
            )
    else:
        if _output_config.format == "json":
            click.echo(json.dumps({"result": str(result)}, indent=2))
        else:
            click.echo(str(result))


@click.group(invoke_without_command=True)
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
@click.option("--csv", "use_csv", is_flag=True, help="Output as CSV.")
@click.option("--output", "-o", "output_file", help="Save output to file.")
@click.option("--limit", "-n", type=int, help="Limit number of rows.")
@click.option("--no-header", is_flag=True, help="Suppress table headers.")
@click.option("--no-cache", is_flag=True, help="禁用结果缓存，强制从 API 获取最新数据。")
@click.option("--retry", type=int, default=0, help="网络失败时自动重试次数 (指数退避，默认 0 不重试)")
@click.version_option(version="0.1.0", prog_name="akshare-cli")
@click.pass_context
def cli(ctx, use_json, use_csv, output_file, limit, no_header, no_cache, retry):
    """AKShare CLI - 命令行金融数据工具

    支持调用 akshare 库的 1090+ 个函数，获取股票、基金、期货、债券、外汇、宏观等金融数据。
    输出选项 (--json, --csv, --output, --limit) 可以放在子命令的前面或后面。

    \b
    基本用法:
        akshare-cli <命令> [参数]          一次性查询
        akshare-cli repl                     进入交互模式

    示例:
        akshare-cli call stock_zh_a_hist --symbol 000001 --json
        akshare-cli stock hist 000001 --json
        akshare-cli --json search fund_etf
        akshare-cli --retry 3 stock spot --json
        akshare-cli macro gdp --csv --output gdp.csv
        akshare-cli repl

    更多信息:
        每个命令都支持 --help 查看详细帮助
        日期参数未指定时会自动填充为当天日期
    """
    ctx.ensure_object(dict)

    _output_config.apply(use_json, use_csv, output_file, limit, no_header)

    if no_cache:
        _result_cache.enabled = False

    if retry > 0:
        set_retry(retry)

    if ctx.invoked_subcommand is None:
        click.echo(ctx.get_help())


# ─── CALL: Dynamic function dispatch ─────────────────────────────────────────


@cli.command("call", context_settings=dict(
    ignore_unknown_options=True,
    allow_extra_args=True,
))
@click.argument("params", nargs=-1, type=click.UNPROCESSED)
@click.pass_context
def cmd_call(ctx, params):
    """调用任意 akshare 函数

    通过函数名直接调用 akshare 库的任意函数。参数以 --key value 形式传入。
    输出选项 (--json/--csv/--output) 可以放在任意位置。
    日期参数未指定时会自动填充为当天日期。

    \b
    用法:
        call <函数名> [--参数 值 ...]

    示例:
        # 获取股票历史数据 (JSON 输出)
        akshare-cli call stock_zh_a_hist --symbol 000001 --json

        # --json 放在前面也可以
        akshare-cli call --json stock_zh_a_spot_em

        # 导出到文件
        akshare-cli call futures_hist_em --symbol 螺纹主连 --output data.csv

        # 获取经济新闻 (自动使用今天日期)
        akshare-cli call news_economic_baidu --json

        # 获取可转债数据，限制 10 行
        akshare-cli call bond_zh_cov --json --limit 10

    查看函数用法:
        akshare-cli call --full-help <函数名>   查看指定函数的完整用法
        akshare-cli search <关键词>              搜索函数
        akshare-cli info <函数名>                查看参数详情
    """
    # First pass: strip output flags from the token list
    remaining = list(params)
    call_output_json = False
    call_output_csv = False
    call_output_file = None
    call_limit = None
    full_help = False

    filtered = []
    i = 0
    while i < len(remaining):
        p = remaining[i]
        if p == "--json":
            call_output_json = True
            i += 1
        elif p == "--csv":
            call_output_csv = True
            i += 1
        elif p == "--full-help":
            full_help = True
            i += 1
        elif p in ("--output", "-o") and i + 1 < len(remaining):
            call_output_file = remaining[i + 1]
            i += 2
        elif p in ("--limit", "-n") and i + 1 < len(remaining):
            try:
                call_limit = int(remaining[i + 1])
            except ValueError:
                pass
            i += 2
        elif p == "--no-header":
            _output_config.no_header = True
            i += 1
        elif p == "--retry" and i + 1 < len(remaining):
            try:
                set_retry(int(remaining[i + 1]))
            except ValueError:
                pass
            i += 2
        else:
            filtered.append(p)
            i += 1

    _output_config.apply(call_output_json, call_output_csv, call_output_file, call_limit)

    # Second pass: the first non-flag token is the function name
    func_name = None
    func_params = []
    for j, tok in enumerate(filtered):
        if not tok.startswith("--"):
            func_name = tok
            func_params = filtered[:j] + filtered[j + 1:]
            break

    # Handle --full-help
    if full_help:
        _print_full_help(func_name)
        return

    if not func_name:
        print_error("Missing function name. Usage: call <func_name> [--param value ...]")
        sys.exit(1)

    kwargs = _parse_extra_params(func_params)
    try:
        result = call_function(func_name, kwargs)
        _handle_result(result, func_name, kwargs)
    except ValueError as e:
        err_msg = str(e)
        if "not found" in err_msg:
            print_error(err_msg)
            suggestions = search_functions(func_name)
            if suggestions:
                click.echo("\n你是不是想找:")
                for s in suggestions[:5]:
                    click.echo(f"  {s}")
        else:
            print_error(f"调用 {func_name} 失败: {e}")
            if os.environ.get("AKSHARE_CLI_DEBUG"):
                traceback.print_exc()
        sys.exit(1)
    except Exception as e:
        print_error(f"调用 {func_name} 失败: {e}")
        if os.environ.get("AKSHARE_CLI_DEBUG"):
            traceback.print_exc()
        sys.exit(1)


def _clean_docstring(docstring: str) -> str:
    """Extract the first meaningful line from an akshare docstring.

    Strips URLs, :param/:type/:return/:rtype lines, and blank lines,
    returning only the human-readable description.
    """
    if not docstring:
        return ""
    lines = []
    for line in docstring.splitlines():
        stripped = line.strip()
        # Stop at first reST field or bare URL
        if stripped.startswith(":") or stripped.startswith("http://") or stripped.startswith("https://"):
            break
        if stripped:
            lines.append(stripped)
    return "\n".join(lines) if lines else ""


def _print_full_help(func_name: Optional[str] = None) -> None:
    """Print detailed usage help for one function or list popular examples."""
    if func_name:
        info = get_function_info(func_name)
        if info is None:
            print_error(f"函数 '{func_name}' 未找到。")
            suggestions = search_functions(func_name)
            if suggestions:
                click.echo("\n你是不是想找:")
                for s in suggestions[:5]:
                    click.echo(f"  {s}")
            sys.exit(1)

        source = get_function_source(info["name"])
        clean_desc = _clean_docstring(info["docstring"])

        if _output_config.format == "json":
            # Build structured help object
            help_obj = {
                "function": info["name"],
                "module": info["module"],
                "description": clean_desc,
                "parameters": [],
                "usage": f"akshare-cli call {info['name']}",
            }
            parts = [f"akshare-cli call {info['name']}"]
            for p in info["params"]:
                pdata = {"name": p["name"]}
                if p.get("type"):
                    pdata["type"] = p["type"]
                if p.get("has_default"):
                    pdata["default"] = p.get("default")
                    pdata["required"] = False
                else:
                    pdata["required"] = True
                help_obj["parameters"].append(pdata)
                if pdata.get("required"):
                    parts.append(f"--{p['name']} <值>")
                else:
                    parts.append(f"[--{p['name']} {p.get('default', '')}]")
            help_obj["usage"] = " ".join(parts)
            if source:
                help_obj["source_url"] = source[0]
                help_obj["source_description"] = source[1]
            click.echo(json.dumps(help_obj, indent=2, ensure_ascii=False, default=str))
        else:
            click.echo(f"函数: {info['name']}")
            click.echo(f"模块: {info['module']}")
            if clean_desc:
                click.echo(f"说明: {clean_desc}")
            if source:
                click.echo(f"数据源: {source[1]}")
                click.echo(f"目标地址: {source[0]}")
            click.echo()

            # Parameter table
            if info["params"]:
                click.echo("参数:")
                for p in info["params"]:
                    name = p["name"]
                    type_str = p.get("type", "Any")
                    if p.get("has_default"):
                        default = p.get("default")
                        click.echo(f"  --{name:<20s} 类型: {type_str:<8s} 默认值: {default!r}")
                    else:
                        click.echo(f"  --{name:<20s} 类型: {type_str:<8s} (必填)")
            else:
                click.echo("参数: 无")
            click.echo()

            # Usage example
            parts = [f"akshare-cli call {info['name']}"]
            for p in info["params"]:
                if not p.get("has_default"):
                    parts.append(f"--{p['name']} <值>")
            parts.append("[--json]")
            click.echo("用法:")
            click.echo(f"  {' '.join(parts)}")
    else:
        # No function name: show general usage
        click.echo("用法: akshare-cli call --full-help <函数名>")
        click.echo()
        click.echo("查看指定函数的完整用法说明，包括参数、类型、默认值。")
        click.echo()
        click.echo("示例:")
        click.echo("  akshare-cli call --full-help stock_zh_a_hist")
        click.echo("  akshare-cli call --full-help news_economic_baidu")
        click.echo("  akshare-cli call --full-help futures_hist_em")
        click.echo("  akshare-cli call --full-help bond_zh_cov --json")
        click.echo()
        click.echo("搜索函数:")
        click.echo("  akshare-cli search <关键词>")


def _parse_extra_params(params):
    """Parse CLI params like --symbol 000001 --period daily into a dict."""
    kwargs = {}
    i = 0
    while i < len(params):
        p = params[i]
        if p.startswith("--"):
            key = p[2:]
            if i + 1 < len(params) and not params[i + 1].startswith("--"):
                kwargs[key] = params[i + 1]
                i += 2
            else:
                kwargs[key] = "true"
                i += 1
        else:
            i += 1
    return kwargs


# ─── SEARCH: Find functions ──────────────────────────────────────────────────


@cli.command("search")
@click.argument("keyword")
@output_options
def cmd_search(keyword):
    """按关键词搜索 akshare 函数

    在 1090+ 个函数中模糊搜索，返回包含关键词的所有函数名及简介。

    \b
    示例:
        # 搜索股票相关函数
        akshare-cli search stock_zh

        # 搜索 ETF 基金函数 (JSON 输出)
        akshare-cli search fund_etf --json

        # 搜索新闻接口
        akshare-cli search news
    """
    results = search_functions(keyword)
    if not results:
        if _output_config.format == "json":
            click.echo(json.dumps({"keyword": keyword, "count": 0, "functions": []}, indent=2))
        else:
            click.echo(f"No functions found matching '{keyword}'.")
        return

    if _output_config.format == "json":
        click.echo(json.dumps({"keyword": keyword, "count": len(results), "functions": results}, indent=2))
    else:
        click.echo(f"Found {len(results)} functions matching '{keyword}':\n")
        for name in results:
            info = get_function_info(name)
            doc_line = ""
            if info and info["docstring"]:
                doc_line = info["docstring"].split("\n")[0][:60]
            click.echo(f"  {name:<50s} {doc_line}")


# ─── LIST: List functions ────────────────────────────────────────────────────


@cli.command("list")
@click.argument("domain", required=False)
@output_options
def cmd_list(domain):
    """列出可用的 akshare 函数

    不指定域时显示所有域的函数数量概览；指定域时列出该域下所有函数。

    \b
    常见域:
        stock     股票        fund      基金
        futures   期货        bond      债券
        forex     外汇        macro     宏观经济
        index     指数        option    期权
        news      新闻

    示例:
        # 查看所有域的概览
        akshare-cli list

        # 列出股票域的所有函数
        akshare-cli list stock

        # JSON 格式输出
        akshare-cli list fund --json
    """
    if domain:
        funcs = list_functions_by_domain(domain)
        if not funcs:
            if _output_config.format == "json":
                click.echo(json.dumps({"domain": domain, "count": 0, "functions": [], "available_domains": get_domains()}, indent=2))
            else:
                click.echo(f"No functions found for domain '{domain}'.")
                click.echo(f"\nAvailable domains: {', '.join(get_domains())}")
            return

        if _output_config.format == "json":
            click.echo(json.dumps({"domain": domain, "count": len(funcs), "functions": funcs}, indent=2))
        else:
            click.echo(f"{domain} ({len(funcs)} functions):\n")
            for name in funcs:
                click.echo(f"  {name}")
    else:
        domains = get_domains()
        all_funcs = get_all_functions()
        if _output_config.format == "json":
            domain_counts = {}
            for d in domains:
                domain_counts[d] = len(list_functions_by_domain(d))
            click.echo(json.dumps({
                "total_functions": len(all_funcs),
                "domains": domain_counts,
            }, indent=2))
        else:
            click.echo(f"AKShare has {len(all_funcs)} functions across {len(domains)} domains:\n")
            for d in domains:
                count = len(list_functions_by_domain(d))
                click.echo(f"  {d:<25s} {count:>4d} functions")
            click.echo(f"\nUse 'list <domain>' to see functions in a domain.")


# ─── INFO: Function details ──────────────────────────────────────────────────


@cli.command("info")
@click.argument("func_name")
@output_options
def cmd_info(func_name):
    """查看函数的详细信息

    显示函数的参数列表、类型、默认值和文档说明。
    在调用陌生函数前先用 info 查看参数要求。

    \b
    示例:
        # 查看股票历史数据函数的参数
        akshare-cli info stock_zh_a_hist

        # JSON 格式输出
        akshare-cli info news_economic_baidu --json

    输出内容:
        - 函数名和模块
        - 参数列表（名称、类型、默认值、是否必填）
        - 函数文档说明
    """
    info = get_function_info(func_name)
    if info is None:
        if _output_config.format == "json":
            click.echo(json.dumps({"error": f"Function '{func_name}' not found"}, indent=2))
        else:
            print_error(f"Function '{func_name}' not found.")
            suggestions = search_functions(func_name)
            if suggestions:
                click.echo("\nDid you mean one of these?")
                for s in suggestions[:5]:
                    click.echo(f"  {s}")
        sys.exit(1)

    if _output_config.format == "json":
        click.echo(json.dumps(info, indent=2, ensure_ascii=False, default=str))
    else:
        click.echo(f"Function: {info['name']}")
        click.echo(f"Module:   {info['module']}")
        click.echo()
        if info["params"]:
            click.echo("Parameters:")
            for p in info["params"]:
                default_str = f" = {p['default']!r}" if p.get("has_default") else " (required)"
                type_str = f" : {p.get('type', 'Any')}" if p.get("type") else ""
                click.echo(f"  --{p['name']}{type_str}{default_str}")
        else:
            click.echo("Parameters: none")
        click.echo()
        if info["docstring"]:
            click.echo("Description:")
            click.echo(f"  {info['docstring']}")


# ─── DOMAIN SHORTCUTS ────────────────────────────────────────────────────────


@cli.group("stock")
def cmd_stock():
    """股票数据快捷命令

    \b
    子命令:
        hist      获取股票历史 K 线数据 (OHLCV)
        spot      获取股票实时行情（支持 --symbol 走快速接口）
        info      获取个股基本信息（⚡0.1s）
        flow      获取个股资金流向
        finance   获取个股财务数据（自动降级）
        holders   获取十大股东
        lhb       获取龙虎榜统计

    示例:
        akshare-cli stock hist 000001 --json
        akshare-cli stock info 600519 --json
        akshare-cli stock spot --symbol 600519 --json
        akshare-cli stock finance 600519 --type profit --json
    """
    pass


@cmd_stock.command("hist")
@click.argument("symbol")
@click.option("--period", default="daily", help="daily/weekly/monthly")
@click.option("--start", "start_date", default="19700101", help="Start date YYYYMMDD")
@click.option("--end", "end_date", default="20500101", help="End date YYYYMMDD")
@click.option("--adjust", default="", help="qfq/hfq/'' (forward/backward/none)")
@output_options
def cmd_stock_hist(symbol, period, start_date, end_date, adjust):
    """获取股票历史 K 线数据

    获取 A 股历史 OHLCV 数据（开盘价、最高价、最低价、收盘价、成交量）。
    数据来源：东方财富 (stock_zh_a_hist)

    \b
    参数:
        SYMBOL        股票代码 (如 000001, 600519)
        --period      周期: daily/weekly/monthly (默认 daily)
        --start       开始日期 YYYYMMDD (默认 19700101)
        --end         结束日期 YYYYMMDD (默认 20500101)
        --adjust      复权: qfq(前复权)/hfq(后复权)/空串(不复权, 默认)

    示例:
        # 平安银行日线数据
        akshare-cli stock hist 000001 --json

        # 贵州茅台周线 + 前复权
        akshare-cli stock hist 600519 --period weekly --adjust qfq

        # 指定日期范围
        akshare-cli stock hist 000001 --start 20260101 --end 20260311 --csv
    """
    kwargs = {
        "symbol": symbol,
        "period": period,
        "start_date": start_date,
        "end_date": end_date,
        "adjust": adjust,
    }
    try:
        result = call_function("stock_zh_a_hist", kwargs)
        _handle_result(result, "stock_zh_a_hist", kwargs)
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cmd_stock.command("spot")
@click.option("--market", default="all", help="all/sh/sz/bj/hk/us")
@click.option("--symbol", default=None, help="单只股票代码（走快速接口 stock_individual_info_em）")
@output_options
def cmd_stock_spot(market, symbol):
    """获取股票实时行情

    获取各市场股票实时报价数据。数据来源：东方财富。
    传入 --symbol 时走 stock_individual_info_em（⚡0.1s），
    不传则走全量接口（较慢，5000+ 行）。

    \b
    参数:
        --symbol    单只股票代码 (如 600519)，使用快速接口
        --market    市场选择 (默认 all)
                    all  全部 A 股
                    sh   上海 A 股
                    sz   深圳 A 股
                    bj   北交所
                    hk   港股
                    us   美股

    示例:
        akshare-cli stock spot --symbol 600519 --json
        akshare-cli stock spot --json
        akshare-cli stock spot --market sh --csv
        akshare-cli stock spot --market hk --json --limit 20

    注意: 全市场数据量较大 (5000+ 行)，建议配合 --limit 或 --symbol 使用
    """
    if symbol:
        func_name = "stock_individual_info_em"
        kw = {"symbol": symbol}
    else:
        func_map = {
            "all": "stock_zh_a_spot_em",
            "sh": "stock_sh_a_spot_em",
            "sz": "stock_sz_a_spot_em",
            "bj": "stock_bj_a_spot_em",
            "hk": "stock_hk_spot_em",
            "us": "stock_us_spot_em",
        }
        func_name = func_map.get(market, "stock_zh_a_spot_em")
        kw = {}
    try:
        result = call_function(func_name, kw)
        _handle_result(result, func_name, kw)
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cli.group("fund")
def cmd_fund():
    """基金数据快捷命令

    \b
    子命令:
        etf     获取 ETF 基金数据

    示例:
        akshare-cli fund etf --json            列出所有 ETF
        akshare-cli fund etf 159707 --json     获取单只 ETF 历史数据
    """
    pass


@cmd_stock.command("info")
@click.argument("symbol")
@output_options
def cmd_stock_info(symbol):
    """获取个股基本信息

    获取单只股票的基本信息（⚡0.1s）。数据来源：东方财富 (stock_individual_info_em)

    \b
    参数:
        SYMBOL    股票代码 (如 600519, 000001)

    示例:
        akshare-cli stock info 600519 --json
        akshare-cli stock info 000001
    """
    try:
        result = call_function("stock_individual_info_em", {"symbol": symbol})
        _handle_result(result, "stock_individual_info_em", {"symbol": symbol})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cmd_stock.command("flow")
@click.argument("symbol")
@output_options
def cmd_stock_flow(symbol):
    """获取个股资金流向

    获取单只股票的资金流向数据。数据来源：东方财富 (stock_individual_fund_flow)

    \b
    参数:
        SYMBOL    股票代码 (如 600519, 000001)

    示例:
        akshare-cli stock flow 600519 --json
        akshare-cli stock flow 000001 --csv
    """
    try:
        result = call_function("stock_individual_fund_flow", {"stock": symbol, "market": "sh" if symbol.startswith("6") else "sz"})
        _handle_result(result, "stock_individual_fund_flow", {"stock": symbol})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cmd_stock.command("finance")
@click.argument("symbol")
@click.option("--type", "finance_type", default="indicator",
              help="indicator/profit/balance/cashflow")
@output_options
def cmd_stock_finance(symbol, finance_type):
    """获取个股财务数据

    获取单只股票的财务报表数据。自动降级到可用数据源。

    \b
    参数:
        SYMBOL          股票代码 (如 600519, 000001)
        --type          报表类型 (默认 indicator)
                        indicator  财务分析指标
                        profit     利润表
                        balance    资产负债表
                        cashflow   现金流量表

    示例:
        akshare-cli stock finance 600519 --json
        akshare-cli stock finance 000001 --type profit --json
        akshare-cli stock finance 600519 --type balance --csv
    """
    func_map = {
        "indicator": "stock_financial_analysis_indicator_em",
        "profit": "stock_profit_sheet_by_report_em",
        "balance": "stock_balance_sheet_by_report_em",
        "cashflow": "stock_cash_flow_sheet_by_report_em",
    }
    func_name = func_map.get(finance_type, "stock_financial_analysis_indicator_em")
    try:
        result = call_function(func_name, {"symbol": symbol})
        _handle_result(result, func_name, {"symbol": symbol})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cmd_stock.command("holders")
@click.argument("symbol")
@output_options
def cmd_stock_holders(symbol):
    """获取十大股东信息

    获取单只股票的十大股东数据。数据来源：东方财富 (stock_main_stock_holder)

    \b
    参数:
        SYMBOL    股票代码 (如 600519, 000001)

    示例:
        akshare-cli stock holders 600519 --json
        akshare-cli stock holders 000001 --csv
    """
    try:
        result = call_function("stock_main_stock_holder", {"stock": symbol})
        _handle_result(result, "stock_main_stock_holder", {"stock": symbol})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cmd_stock.command("lhb")
@click.option("--start", "start_date", default=None, help="开始日期 YYYYMMDD")
@click.option("--end", "end_date", default=None, help="结束日期 YYYYMMDD")
@output_options
def cmd_stock_lhb(start_date, end_date):
    """获取龙虎榜统计数据

    获取龙虎榜个股上榜统计。数据来源：东方财富 (stock_lhb_stock_statistic_em)

    \b
    参数:
        --start    开始日期 YYYYMMDD (默认自动填充)
        --end      结束日期 YYYYMMDD (默认自动填充)

    示例:
        akshare-cli stock lhb --json
        akshare-cli stock lhb --start 20260301 --end 20260314 --json
    """
    kw = {}
    if start_date:
        kw["start_date"] = start_date
    if end_date:
        kw["end_date"] = end_date
    try:
        result = call_function("stock_lhb_stock_statistic_em", kw)
        _handle_result(result, "stock_lhb_stock_statistic_em", kw)
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cli.group("sector")
def cmd_sector():
    """板块数据快捷命令

    \b
    子命令:
        concept     概念板块列表
        industry    行业板块列表

    示例:
        akshare-cli sector concept --json --limit 10
        akshare-cli sector industry --json
    """
    pass


@cmd_sector.command("concept")
@output_options
def cmd_sector_concept():
    """获取概念板块列表

    获取东方财富概念板块名称列表。数据来源：stock_board_concept_name_em

    \b
    示例:
        akshare-cli sector concept --json
        akshare-cli sector concept --json --limit 20
        akshare-cli sector concept --csv --output concepts.csv
    """
    try:
        result = call_function("stock_board_concept_name_em", {})
        _handle_result(result, "stock_board_concept_name_em", {})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cmd_sector.command("industry")
@output_options
def cmd_sector_industry():
    """获取行业板块列表

    获取东方财富行业板块名称列表。数据来源：stock_board_industry_name_em

    \b
    示例:
        akshare-cli sector industry --json
        akshare-cli sector industry --json --limit 20
        akshare-cli sector industry --csv --output industries.csv
    """
    try:
        result = call_function("stock_board_industry_name_em", {})
        _handle_result(result, "stock_board_industry_name_em", {})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cmd_fund.command("etf")
@click.argument("symbol", required=False)
@click.option("--period", default="daily", help="daily/weekly/monthly")
@click.option("--start", "start_date", default="19700101", help="Start date")
@click.option("--end", "end_date", default="20500101", help="End date")
@click.option("--adjust", default="", help="qfq/hfq/''")
@output_options
def cmd_fund_etf(symbol, period, start_date, end_date, adjust):
    """获取 ETF 基金数据

    不传代码时列出所有 ETF 实时行情；传代码时获取单只 ETF 历史数据。
    数据来源：东方财富

    \b
    参数:
        [SYMBOL]      ETF 代码 (可选，如 159707, 510300)
        --period      周期: daily/weekly/monthly (默认 daily)
        --start       开始日期 YYYYMMDD
        --end         结束日期 YYYYMMDD
        --adjust      复权: qfq/hfq/空串

    示例:
        # 列出所有 ETF 实时行情
        akshare-cli fund etf --json

        # 获取单只 ETF 历史数据
        akshare-cli fund etf 159707 --json

        # 前复权周线数据
        akshare-cli fund etf 510300 --period weekly --adjust qfq --csv
    """
    if symbol:
        kwargs = {
            "symbol": symbol,
            "period": period,
            "start_date": start_date,
            "end_date": end_date,
            "adjust": adjust,
        }
        try:
            result = call_function("fund_etf_hist_em", kwargs)
            _handle_result(result, "fund_etf_hist_em", kwargs)
        except Exception as e:
            print_error(f"Failed: {e}")
            sys.exit(1)
    else:
        try:
            result = call_function("fund_etf_spot_em", {})
            _handle_result(result, "fund_etf_spot_em", {})
        except Exception as e:
            print_error(f"Failed: {e}")
            sys.exit(1)


@cli.group("futures")
def cmd_futures():
    """期货数据快捷命令

    \b
    子命令:
        hist    获取期货历史 K 线数据
        list    列出可用期货合约

    示例:
        akshare-cli futures hist 螺纹主连 --json
        akshare-cli futures list --json
    """
    pass


@cmd_futures.command("hist")
@click.argument("symbol")
@click.option("--period", default="daily", help="daily/weekly/monthly")
@click.option("--start", "start_date", default="19900101", help="Start date")
@click.option("--end", "end_date", default="20500101", help="End date")
@output_options
def cmd_futures_hist(symbol, period, start_date, end_date):
    """获取期货历史 K 线数据

    数据来源：东方财富 (futures_hist_em)

    \b
    参数:
        SYMBOL        期货合约名称 (如 螺纹主连, 热卷主连)
        --period      周期: daily/weekly/monthly (默认 daily)
        --start       开始日期 YYYYMMDD
        --end         结束日期 YYYYMMDD

    示例:
        akshare-cli futures hist 螺纹主连 --json
        akshare-cli futures hist 热卷主连 --period weekly --csv
        akshare-cli futures hist 沥青主连 --start 20260101 --json

    提示: 先用 'futures list' 查看可用合约名称
    """
    kwargs = {
        "symbol": symbol,
        "period": period,
        "start_date": start_date,
        "end_date": end_date,
    }
    try:
        result = call_function("futures_hist_em", kwargs)
        _handle_result(result, "futures_hist_em", kwargs)
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cmd_futures.command("list")
@output_options
def cmd_futures_list():
    """列出可用期货合约

    显示所有可查询的期货合约名称。数据来源：东方财富 (futures_hist_table_em)

    \b
    示例:
        akshare-cli futures list --json
        akshare-cli futures list --csv --output contracts.csv
    """
    try:
        result = call_function("futures_hist_table_em", {})
        _handle_result(result, "futures_hist_table_em", {})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cli.group("bond")
def cmd_bond():
    """债券数据快捷命令

    \b
    子命令:
        convertible   获取可转债数据

    示例:
        akshare-cli bond convertible --json

    更多债券函数可通过 call 命令调用:
        akshare-cli call bond_china_yield --json
        akshare-cli call bond_zh_us_rate --json
    """
    pass


@cmd_bond.command("convertible")
@output_options
def cmd_bond_convertible():
    """获取可转债数据

    获取全市场可转债实时数据。数据来源：集思录 (bond_cb_jsl)

    \b
    示例:
        akshare-cli bond convertible --json
        akshare-cli bond convertible --csv --output bonds.csv
        akshare-cli bond convertible --json --limit 20
    """
    try:
        result = call_function("bond_cb_jsl", {})
        _handle_result(result, "bond_cb_jsl", {})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cli.group("index")
def cmd_index():
    """指数数据快捷命令

    \b
    子命令:
        spot    获取指数实时行情

    示例:
        akshare-cli index spot --json            全球指数
        akshare-cli index spot --market cn --json  中国指数
    """
    pass


@cmd_index.command("spot")
@click.option("--market", default="global", help="global/cn")
@output_options
def cmd_index_spot(market):
    """获取指数实时行情

    \b
    参数:
        --market    市场选择 (默认 global)
                    global  全球主要指数 (index_global_spot_em)
                    cn      中国指数 (stock_zh_index_spot_em)

    示例:
        akshare-cli index spot --json
        akshare-cli index spot --market cn --json --limit 20
    """
    func_name = "index_global_spot_em" if market == "global" else "stock_zh_index_spot_em"
    try:
        result = call_function(func_name, {})
        _handle_result(result, func_name, {})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cli.group("macro")
def cmd_macro():
    """宏观经济数据快捷命令

    \b
    子命令:
        gdp     中国 GDP 数据
        cpi     中国 CPI 数据 (支持月度/年度)

    示例:
        akshare-cli macro gdp --json
        akshare-cli macro cpi --freq yearly --csv

    更多宏观数据可通过 call 命令调用:
        akshare-cli call macro_china_ppi_yearly --json
        akshare-cli call macro_china_lpr --json
        akshare-cli call macro_china_m2_yearly --json
    """
    pass


@cmd_macro.command("gdp")
@output_options
def cmd_macro_gdp():
    """获取中国 GDP 数据

    获取中国年度 GDP 数据。数据来源：macro_china_gdp_yearly

    \b
    示例:
        akshare-cli macro gdp --json
        akshare-cli macro gdp --csv --output gdp.csv
    """
    try:
        result = call_function("macro_china_gdp_yearly", {})
        _handle_result(result, "macro_china_gdp_yearly", {})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cmd_macro.command("cpi")
@click.option("--freq", default="monthly", help="monthly/yearly")
@output_options
def cmd_macro_cpi(freq):
    """获取中国 CPI 数据

    获取中国消费者物价指数 (CPI) 数据。

    \b
    参数:
        --freq    频率: monthly(月度)/yearly(年度) (默认 monthly)

    示例:
        # 月度 CPI
        akshare-cli macro cpi --json

        # 年度 CPI
        akshare-cli macro cpi --freq yearly --csv
    """
    func_name = f"macro_china_cpi_{freq}"
    try:
        result = call_function(func_name, {})
        _handle_result(result, func_name, {})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cli.group("forex")
def cmd_forex():
    """外汇数据快捷命令

    \b
    子命令:
        spot    获取外汇实时汇率
        hist    获取外汇历史汇率

    示例:
        akshare-cli forex spot --json
        akshare-cli forex hist USDCNH --json
    """
    pass


@cmd_forex.command("spot")
@output_options
def cmd_forex_spot():
    """获取外汇实时汇率

    获取主要货币对实时汇率。数据来源：东方财富 (forex_spot_em)

    \b
    示例:
        akshare-cli forex spot --json
        akshare-cli forex spot --csv --limit 10
    """
    try:
        result = call_function("forex_spot_em", {})
        _handle_result(result, "forex_spot_em", {})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cmd_forex.command("hist")
@click.argument("symbol", default="USDCNH")
@output_options
def cmd_forex_hist(symbol):
    """获取外汇历史汇率

    数据来源：东方财富 (forex_hist_em)

    \b
    参数:
        [SYMBOL]    货币对 (默认 USDCNH)
                    常用: USDCNH, EURUSD, GBPUSD, USDJPY

    示例:
        akshare-cli forex hist USDCNH --json
        akshare-cli forex hist EURUSD --csv
    """
    kwargs = {"symbol": symbol}
    try:
        result = call_function("forex_hist_em", kwargs)
        _handle_result(result, "forex_hist_em", kwargs)
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


@cli.group("option")
def cmd_option():
    """期权数据快捷命令

    \b
    子命令:
        info    获取期权合约信息

    示例:
        akshare-cli option info --json
    """
    pass


@cmd_option.command("info")
@output_options
def cmd_option_info():
    """获取期权合约信息

    获取期权合约基本信息。数据来源：option_contract_info_ctp

    \b
    示例:
        akshare-cli option info --json
        akshare-cli option info --csv --output options.csv
    """
    try:
        result = call_function("option_contract_info_ctp", {})
        _handle_result(result, "option_contract_info_ctp", {})
    except Exception as e:
        print_error(f"Failed: {e}")
        sys.exit(1)


# ─── EXPORT ──────────────────────────────────────────────────────────────────


@cli.command("export")
@click.argument("filepath")
def cmd_export(filepath):
    """导出上一次查询结果到文件

    将最近一次查询返回的 DataFrame 导出到指定文件。

    \b
    支持格式:
        .csv    CSV 文件
        .json   JSON 文件
        .xlsx   Excel 文件
        .md     Markdown 表格

    示例:
        akshare-cli export result.csv
        akshare-cli export data.xlsx
        akshare-cli export report.md

    注意: 需要先运行查询命令，否则无数据可导出
    """
    if _session.last_result is None:
        print_error("No data to export. Run a query first.")
        sys.exit(1)

    try:
        saved = auto_export(_session.last_result, filepath)
        click.echo(f"Exported to {saved}")
    except Exception as e:
        print_error(f"Export failed: {e}")
        sys.exit(1)


# ─── REPL ────────────────────────────────────────────────────────────────────


@cli.command("repl")
def cmd_repl():
    """启动交互式 REPL 会话

    进入交互模式，可以连续执行多个查询，保留会话状态。

    \b
    REPL 命令:
        call <函数> [--参数 值 ...]  调用函数
        search <关键词>               搜索函数
        list [域]                      列出函数
        info <函数>                    查看函数详情
        export <文件路径>              导出上次结果
        history                         查看历史记录
        set <键> <值>                   设置偏好
        json on|off                     切换 JSON 输出
        help                            显示帮助
        quit / exit / q                 退出

    所有命令都支持 --json, --csv, --output 输出选项。

    示例:
        akshare-cli repl
    """
    click.echo("AKShare CLI REPL - Type 'help' for commands, 'quit' to exit.")
    click.echo(f"Loaded {len(get_all_functions())} functions.\n")

    while True:
        try:
            line = input("akshare> ").strip()
        except (EOFError, KeyboardInterrupt):
            click.echo("\nBye!")
            break

        if not line:
            continue

        if line in ("quit", "exit", "q"):
            click.echo("Bye!")
            break

        if line == "help":
            click.echo(
                "Commands:\n"
                "  call <func> [--param value ...]  Call function\n"
                "  search <keyword>                 Search functions\n"
                "  list [domain]                    List functions\n"
                "  info <func>                      Function details\n"
                "  export <filepath>                Export last result\n"
                "  history                          Show history\n"
                "  set <key> <value>                Set preference\n"
                "  json on|off                      Toggle JSON output\n"
                "  quit                             Exit\n"
                "\nAll commands support --json, --csv, --output flags.\n"
            )
            continue

        if line == "history":
            for i, entry in enumerate(_session.history):
                click.echo(
                    f"  {i+1}. [{entry['timestamp'][:19]}] {entry['function']} "
                    f"-> {entry.get('rows', '?')} rows"
                )
            continue

        if line.startswith("json "):
            mode = line.split()[1]
            if mode == "on":
                _output_config.format = "json"
                click.echo("JSON output enabled.")
            else:
                _output_config.format = "table"
                click.echo("Table output enabled.")
            continue

        if line.startswith("set "):
            parts = line.split(maxsplit=2)
            if len(parts) == 3:
                _session.set_preference(parts[1], parts[2])
                click.echo(f"Set {parts[1]} = {parts[2]}")
            else:
                click.echo("Usage: set <key> <value>")
            continue

        # Parse and dispatch REPL commands through Click
        try:
            args = shlex.split(line)
        except ValueError:
            args = line.split()

        if not args:
            continue

        try:
            cli.main(args=args, standalone_mode=False)
        except SystemExit:
            pass
        except Exception as e:
            print_error(str(e))
            if os.environ.get("AKSHARE_CLI_DEBUG"):
                traceback.print_exc()


# ─── VERSION ─────────────────────────────────────────────────────────────────


@cli.command("version")
@output_options
def cmd_version():
    """显示版本信息

    显示 CLI 工具和 akshare 库的版本号。

    \b
    示例:
        akshare-cli version
        akshare-cli version --json
    """
    from akshare_cli import __version__ as harness_version

    info = {"cli_harness_version": harness_version}
    try:
        ak_mod = __import__("akshare")
        info["akshare_version"] = ak_mod.__version__
    except ImportError:
        info["akshare_version"] = "not installed"

    if _output_config.format == "json":
        click.echo(json.dumps(info, indent=2))
    else:
        for k, v in info.items():
            click.echo(f"{k}: {v}")


# ─── CACHE MANAGEMENT ────────────────────────────────────────────────────────


@cli.group("cache")
def cmd_cache():
    """管理结果缓存

    查看缓存统计信息或清空缓存。API 调用结果会自动缓存以加速重复查询。
    实时行情类接口不会被缓存。

    \b
    子命令:
        stats    显示缓存命中率等统计信息
        clear    清空所有缓存

    示例:
        akshare-cli cache stats
        akshare-cli cache clear
    """
    pass


@cmd_cache.command("stats")
def cache_stats():
    """显示缓存统计信息

    \b
    示例:
        akshare-cli cache stats
    """
    stats = _result_cache.stats()
    if _output_config.format == "json":
        click.echo(json.dumps(stats, indent=2, ensure_ascii=False))
    else:
        click.echo(f"缓存状态:  {'启用' if stats['enabled'] else '禁用'}")
        click.echo(f"缓存条目:  {stats['entries']}")
        click.echo(f"命中次数:  {stats['hits']}")
        click.echo(f"未命中:    {stats['misses']}")
        click.echo(f"命中率:    {stats['hit_rate']}")


@cmd_cache.command("clear")
def cache_clear():
    """清空所有缓存

    \b
    示例:
        akshare-cli cache clear
    """
    _result_cache.clear()
    click.echo("缓存已清空。")


def main():
    """Entry point for console_scripts."""
    cli()


if __name__ == "__main__":
    main()
