from __future__ import annotations

import argparse
import json
import re
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen


# ===== 直接运行时主要改这里 =====
API_BASE = "http://127.0.0.1:8000"
OUTPUT_PATH: str | Path | None = None
OUTPUT_DIR: str | Path | None = None
YEAR = 2026
MONTH = 5
CT = "是"
SCOPE = "行政区"
SCOPE_VALUE = "崇明"
TIMEOUT = 15.0
# ================================

VALID_SCOPES = {"上海", "行政区", "属地"}
VALID_CT_VALUES = {"是", "否"}


class AnalysisTxtError(RuntimeError):
    """Raised when analysis txt generation cannot continue."""


@dataclass(frozen=True)
class ApiCall:
    path: str
    params: dict[str, object]


@dataclass(frozen=True)
class AnalysisTask:
    filename: str
    prompt: str
    calls: tuple[ApiCall, ...]


@dataclass(frozen=True)
class RunConfig:
    api_base: str
    output_path: Path
    year: int
    month: int
    ct: str
    scope: str
    scope_value: str | None
    timeout: float


Fetcher = Callable[[str, str, dict[str, object], float], object]


def _normalize_month(month: int) -> int:
    if not 1 <= month <= 12:
        raise ValueError("month 必须在 1-12 之间")
    return month


def _normalize_scope(scope: str, scope_value: str | None) -> tuple[str, str | None]:
    normalized_scope = scope.strip()
    if normalized_scope not in VALID_SCOPES:
        raise ValueError("scope 必须是 上海、行政区 或 属地")

    normalized_value = scope_value.strip() if scope_value is not None else ""
    if normalized_scope == "上海":
        return normalized_scope, None

    if not normalized_value:
        raise ValueError(f"scope 为{normalized_scope}时 scope_value 不能为空")
    return normalized_scope, normalized_value


def _normalize_ct(ct: str) -> str:
    normalized_ct = ct.strip()
    if normalized_ct not in VALID_CT_VALUES:
        raise ValueError("ct 必须是 是 或 否")
    return normalized_ct


def _scope_label(scope: str, scope_value: str | None) -> str:
    if scope == "上海":
        return "上海"
    return f"{scope}（{scope_value}）"


def _safe_filename_segment(value: str) -> str:
    safe_value = re.sub(r'[\\/:*?"<>|]+', "_", value)
    safe_value = re.sub(r"\s+", "_", safe_value).strip("._ ")
    return safe_value or "未命名"


def _base_params(
    *,
    year: int,
    month: int,
    scope: str,
    scope_value: str | None,
) -> dict[str, object]:
    params: dict[str, object] = {
        "year": year,
        "month": month,
        "scope": scope,
    }
    if scope_value is not None:
        params["scope_value"] = scope_value
    return params


def build_analysis_tasks(
    *,
    year: int,
    month: int,
    ct: str,
    scope: str,
    scope_value: str | None,
) -> list[AnalysisTask]:
    normalized_month = _normalize_month(month)
    normalized_ct = _normalize_ct(ct)
    normalized_scope, normalized_scope_value = _normalize_scope(scope, scope_value)
    label = _scope_label(normalized_scope, normalized_scope_value)
    tender_params = _base_params(
        year=year,
        month=normalized_month,
        scope=normalized_scope,
        scope_value=normalized_scope_value,
    )
    award_params = {
        **tender_params,
        "ct": normalized_ct,
    }

    return [
        AnalysisTask(
            filename="01_公开市场整体态势.txt",
            prompt=f"请基于以下数据分析{label}公开市场整体态势：",
            calls=(
                ApiCall("/tender/overall", dict(tender_params)),
                ApiCall("/tender/region", dict(tender_params)),
            ),
        ),
        AnalysisTask(
            filename="02_公开市场行业分布.txt",
            prompt=f"请基于以下数据分析{label}公开市场行业分布：",
            calls=(ApiCall("/tender/industry", dict(tender_params)),),
        ),
        AnalysisTask(
            filename="03_公开市场单月动向.txt",
            prompt=f"请基于以下数据分析{label}公开市场单月动向：",
            calls=(ApiCall("/tender/overall", dict(tender_params)),),
        ),
        AnalysisTask(
            filename="04_运营商市场整体态势.txt",
            prompt=f"请基于以下数据分析{label}运营商市场整体态势：",
            calls=(ApiCall("/award/overall", dict(award_params)),),
        ),
        AnalysisTask(
            filename="05_运营商市场行业分布.txt",
            prompt=f"请基于以下数据分析{label}运营商市场行业分布：",
            calls=(ApiCall("/award/industry", dict(award_params)),),
        ),
        AnalysisTask(
            filename="06_运营商市场单月动向.txt",
            prompt=f"请基于以下数据分析{label}运营商市场单月动向：",
            calls=(ApiCall("/award/overall", dict(award_params)),),
        ),
    ]


def _request_url(api_base: str, path: str, params: dict[str, object]) -> str:
    query = urlencode(params)
    return f"{api_base.rstrip('/')}{path}?{query}"


def fetch_json(
    api_base: str,
    path: str,
    params: dict[str, object],
    timeout: float,
) -> object:
    url = _request_url(api_base, path, params)
    request = Request(url, headers={"Accept": "application/json"})
    try:
        with urlopen(request, timeout=timeout) as response:
            payload = response.read().decode("utf-8")
    except HTTPError as exc:
        return _http_error_payload(exc)
    except URLError as exc:
        raise AnalysisTxtError(f"无法连接 FastAPI：{url}，原因：{exc.reason}") from exc

    try:
        return json.loads(payload)
    except json.JSONDecodeError as exc:
        raise AnalysisTxtError(f"接口返回的内容不是 JSON：{url}") from exc


def _http_error_payload(exc: HTTPError) -> dict[str, object]:
    body = exc.read().decode("utf-8", errors="replace")
    try:
        response_content: object = json.loads(body)
    except json.JSONDecodeError:
        response_content = body
    return {
        "HTTP状态码": exc.code,
        "错误信息": f"接口返回 HTTP {exc.code}",
        "响应内容": response_content,
    }


def render_task(task: AnalysisTask, results: Sequence[object]) -> str:
    lines = [task.prompt, ""]
    for call, result in zip(task.calls, results, strict=True):
        lines.extend(
            [
                f"调用接口GET {call.path} 记录数据结果",
                "请求参数：",
                json.dumps(call.params, ensure_ascii=False, indent=2),
                "数据结果：",
                json.dumps(result, ensure_ascii=False, indent=2),
                "",
            ]
        )
    return "\n".join(lines).rstrip() + "\n"


def render_unified_txt(task_results: Sequence[tuple[AnalysisTask, Sequence[object]]]) -> str:
    return "\n\n".join(
        render_task(task, results).rstrip()
        for task, results in task_results
    ) + "\n"


def default_output_filename(
    *,
    year: int,
    month: int,
    scope: str,
    scope_value: str | None,
) -> str:
    filename_parts = [
        str(year),
        str(month),
        _safe_filename_segment(scope),
    ]
    if scope_value is not None and scope_value.strip():
        filename_parts.append(_safe_filename_segment(scope_value))
    return f"{'_'.join(filename_parts)}数据分析结果.txt"


def default_output_path(
    *,
    year: int,
    month: int,
    scope: str,
    scope_value: str | None,
) -> Path:
    return Path("outputs") / default_output_filename(
        year=year,
        month=month,
        scope=scope,
        scope_value=scope_value,
    )


def default_output_dir(
    *,
    year: int,
    month: int,
    scope: str,
    scope_value: str | None,
) -> Path:
    return default_output_path(
        year=year,
        month=month,
        scope=scope,
        scope_value=scope_value,
    ).parent


def generate_files(
    *,
    api_base: str,
    year: int,
    month: int,
    ct: str,
    scope: str,
    scope_value: str | None,
    timeout: float,
    output_path: Path | None = None,
    output_dir: Path | None = None,
    fetcher: Fetcher = fetch_json,
) -> list[Path]:
    tasks = build_analysis_tasks(
        year=year,
        month=month,
        ct=ct,
        scope=scope,
        scope_value=scope_value,
    )
    if output_path is None:
        if output_dir is None:
            output_path = default_output_path(
                year=year,
                month=month,
                scope=scope,
                scope_value=scope_value,
            )
        else:
            output_path = output_dir / default_output_filename(
                year=year,
                month=month,
                scope=scope,
                scope_value=scope_value,
            )
    output_path.parent.mkdir(parents=True, exist_ok=True)

    task_results: list[tuple[AnalysisTask, Sequence[object]]] = []
    for task in tasks:
        results = [
            fetcher(api_base, call.path, call.params, timeout)
            for call in task.calls
        ]
        task_results.append((task, results))

    output_path.write_text(render_unified_txt(task_results), encoding="utf-8")
    written_files = [output_path]
    return written_files


def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="调用 marketAPI 并生成数据分析基础 txt 文件。",
    )
    parser.add_argument("--api-base", help="覆盖脚本顶部的 API_BASE。")
    parser.add_argument("--output-path", type=Path, help="覆盖脚本顶部的 OUTPUT_PATH。")
    parser.add_argument(
        "--output-dir",
        type=Path,
        help="自定义输出目录，文件名按规则生成。",
    )
    parser.add_argument("--year", type=int, help="覆盖脚本顶部的 YEAR。")
    parser.add_argument("--month", type=int, help="覆盖脚本顶部的 MONTH。")
    parser.add_argument("--ct", help="覆盖脚本顶部的 CT。")
    parser.add_argument("--scope", help="覆盖脚本顶部的 SCOPE。")
    parser.add_argument("--scope-value", help="覆盖脚本顶部的 SCOPE_VALUE。")
    parser.add_argument(
        "--timeout",
        type=float,
        help="覆盖脚本顶部的 TIMEOUT。",
    )
    return parser.parse_args(argv)


def _output_path_from_config(
    output_path: str | Path | None,
    output_dir: str | Path | None,
    *,
    year: int,
    month: int,
    scope: str,
    scope_value: str | None,
) -> Path:
    if output_path is not None and str(output_path).strip():
        return Path(output_path)

    if output_dir is not None and str(output_dir).strip():
        return Path(output_dir) / default_output_filename(
            year=year,
            month=month,
            scope=scope,
            scope_value=scope_value,
        )

    return default_output_path(
        year=year,
        month=month,
        scope=scope,
        scope_value=scope_value,
    )


def resolve_run_config(args: argparse.Namespace) -> RunConfig:
    year = args.year if args.year is not None else YEAR
    month = _normalize_month(args.month if args.month is not None else MONTH)
    ct = _normalize_ct(args.ct if args.ct is not None else CT)
    scope_input = args.scope if args.scope is not None else SCOPE
    scope_value_input = args.scope_value if args.scope_value is not None else SCOPE_VALUE
    output_path_input = args.output_path if args.output_path is not None else OUTPUT_PATH
    output_dir_input = args.output_dir if args.output_dir is not None else OUTPUT_DIR
    timeout = args.timeout if args.timeout is not None else TIMEOUT
    api_base = args.api_base if args.api_base is not None else API_BASE

    scope = scope_input.strip()
    scope, scope_value = _normalize_scope(scope, scope_value_input)
    output_path = _output_path_from_config(
        output_path_input,
        output_dir_input,
        year=year,
        month=month,
        scope=scope,
        scope_value=scope_value,
    )

    return RunConfig(
        api_base=api_base,
        output_path=output_path,
        year=year,
        month=month,
        ct=ct,
        scope=scope,
        scope_value=scope_value,
        timeout=timeout,
    )


def main(argv: Sequence[str] | None = None) -> int:
    config = resolve_run_config(parse_args(argv))

    written_files = generate_files(
        api_base=config.api_base,
        output_path=config.output_path,
        year=config.year,
        month=config.month,
        ct=config.ct,
        scope=config.scope,
        scope_value=config.scope_value,
        timeout=config.timeout,
    )

    print(f"已生成 {len(written_files)} 个 txt 文件：")
    for path in written_files:
        print(path)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
