# -*- coding: utf-8 -*-
"""
===================================
股票分析接口
===================================

职责：
1. 提供 POST /api/v1/analysis/analyze 触发分析接口
2. 提供 GET /api/v1/analysis/status/{task_id} 查询任务状态接口
3. 提供 GET /api/v1/analysis/tasks 获取任务列表接口
4. 提供 GET /api/v1/analysis/tasks/stream SSE 实时推送接口

特性：
- 异步任务队列：分析任务异步执行，不阻塞请求
- 防重复提交：相同股票代码正在分析时返回 409
- SSE 实时推送：任务状态变化实时通知前端
"""

import asyncio
import copy
import json
import logging
import re
import uuid
from datetime import datetime
from pathlib import Path
from typing import Optional, Union, Dict, Any

from fastapi import APIRouter, HTTPException, Depends, Query, Body
from fastapi.responses import JSONResponse, StreamingResponse

from api.deps import get_config_dep
from api.v1.errors import api_error
from api.v1.schemas.analysis import (
    AnalyzeRequest,
    AnalysisResultResponse,
    TaskAccepted,
    BatchTaskAcceptedResponse,
    BatchTaskAcceptedItem,
    BatchDuplicateTaskItem,
    TaskStatus,
    TaskInfo,
    TaskListResponse,
    DuplicateTaskErrorResponse,
    MarketReviewRequest,
    MarketReviewAccepted,
)
from api.v1.schemas.common import ErrorResponse
from api.v1.schemas.history import (
    AnalysisReport,
    ReportMeta,
    ReportSummary,
    ReportStrategy,
    ReportDetails,
)
from api.v1.schemas.run_flow import RunFlowSnapshot
from data_provider.base import canonical_stock_code, normalize_stock_code
from src.data.stock_index_loader import resolve_index_stock_code
from src.config import Config
from src.core.market_review_lock import (
    MarketReviewExecutionLock as _MarketReviewExecutionLock,
    market_review_lock_path,
    release_market_review_lock as _release_market_review_lock,
    try_acquire_market_review_lock as _try_acquire_market_review_lock,
)
from src.core.market_review_runtime import (
    build_market_review_runtime as _runtime_build_market_review_runtime,
)
from src.analysis_context_pack_overview import (
    extract_analysis_context_pack_overview,
    sanitize_context_snapshot_for_api,
)
from src.market_phase_summary import (
    extract_market_phase_summary,
    rebuild_market_phase_summary_for_stock_code,
)
from src.services.stock_code_utils import is_code_like, resolve_index_stock_code_for_analysis
from src.report_language import get_localized_stock_name, normalize_report_language
from src.schemas.decision_action import build_action_fields
from src.services.name_to_code_resolver import resolve_name_to_code
from src.services.task_queue import (
    get_task_queue,
    DuplicateTaskError,
    TaskStatus as TaskStatusEnum,
)
from src.services.run_diagnostics import build_run_diagnostic_summary
from src.services.run_flow import build_task_run_flow_snapshot
from src.utils.data_processing import (
    normalize_model_used,
    parse_json_field,
    extract_fundamental_detail_fields,
    extract_board_detail_fields,
    extract_realtime_detail_fields,
)

logger = logging.getLogger(__name__)

router = APIRouter()

_SUPPORTED_FREE_TEXT_RE = re.compile(r"^[A-Za-z0-9.*\-+\u3400-\u9fff\s]+$")


def _get_task_trace_id(task: Any) -> Optional[str]:
    trace_id = getattr(task, "trace_id", None)
    if isinstance(trace_id, str) and trace_id.strip():
        return trace_id
    task_id = getattr(task, "task_id", None)
    if isinstance(task_id, str) and task_id.strip():
        return task_id
    return None


def _market_review_lock_path(config: Config) -> Path:
    return market_review_lock_path(config)


def _build_market_review_runtime(config: Config, source_message: Optional[Any] = None) -> tuple[Any, Any, Any]:
    return _runtime_build_market_review_runtime(config, source_message)


def _with_request_report_language(config: Config, report_language: Optional[str]) -> Config:
    """Return a request-scoped config copy when the caller overrides report language."""
    normalized = normalize_report_language(report_language, default="")
    if not normalized:
        return config

    scoped_config = copy.copy(config)
    scoped_config.report_language = normalized
    return scoped_config


def _run_market_review_background(
    send_notification: bool,
    override_region: Optional[str] = None,
    lock_token: Optional[_MarketReviewExecutionLock] = None,
    config: Optional[Config] = None,
    query_id: Optional[str] = None,
) -> None:
    """Run market review after the API response has been accepted."""
    from src.core.market_review import run_market_review

    runtime_config = config or get_config_dep()
    try:
        notifier, analyzer, search_service = _build_market_review_runtime(runtime_config)
        review_kwargs = {
            "notifier": notifier,
            "analyzer": analyzer,
            "search_service": search_service,
            "config": runtime_config,
            "send_notification": send_notification,
            "override_region": override_region,
            "return_structured": True,
            "trigger_source": "api",
        }
        if query_id:
            review_kwargs["query_id"] = query_id
        logger.info(
            "[MarketReview] component=market_review action=background_start "
            "trigger_source=api task_id=%s region=%s",
            query_id or "-",
            override_region or getattr(runtime_config, "market_review_region", "cn") or "cn",
        )
        report = run_market_review(**review_kwargs)
        if not report:
            raise RuntimeError("大盘复盘未返回可持久化报告")
        if hasattr(report, "report"):
            return {
                "result": report.report,
                "market_review_payload": getattr(report, "market_review_payload", None),
            }
        return {"result": report}
    finally:
        _release_market_review_lock(lock_token)


def _invalid_analysis_input_error() -> HTTPException:
    return api_error(400, "validation_error", "请输入有效的股票代码或股票名称")


def _is_obviously_invalid_analysis_input(text: str) -> bool:
    """Reject mixed alphanumeric noise and unsupported symbols early."""
    if not text or is_code_like(text):
        return False

    if not _SUPPORTED_FREE_TEXT_RE.fullmatch(text):
        return True

    has_letters = any(ch.isalpha() and ch.isascii() for ch in text)
    has_digits = any(ch.isdigit() for ch in text)
    return has_letters and has_digits


def _resolve_and_normalize_input(raw_value: str) -> str:
    """
    Resolve and normalize a stock input for analysis requests.

    Code-like values keep the existing canonical path.
    Non-code inputs must resolve to a known stock code. Obvious garbage
    input is rejected before expensive resolver and task-queue work.
    """
    text = (raw_value or "").strip()
    if not text:
        return ""

    if is_code_like(text):
        return resolve_index_stock_code_for_analysis(text)

    if text.isdigit() and len(text) == 4:
        resolved_index_code = resolve_index_stock_code_for_analysis(text)
        if resolved_index_code != canonical_stock_code(text):
            return resolved_index_code

    if _is_obviously_invalid_analysis_input(text):
        raise _invalid_analysis_input_error()

    resolved = resolve_name_to_code(text)
    if resolved:
        return canonical_stock_code(resolved)

    raise _invalid_analysis_input_error()


# ============================================================
# POST /analyze - 触发股票分析
# ============================================================

@router.post(
    "/analyze",
    response_model=AnalysisResultResponse,
    responses={
        200: {"description": "分析完成（同步模式）", "model": AnalysisResultResponse},
        202: {
            "description": "分析任务已接受（异步模式）",
            "model": Union[TaskAccepted, BatchTaskAcceptedResponse],
        },
        400: {"description": "请求参数错误", "model": ErrorResponse},
        409: {"description": "股票正在分析中，拒绝重复提交", "model": DuplicateTaskErrorResponse},
        500: {"description": "分析失败", "model": ErrorResponse},
    },
    summary="触发股票分析",
    description="启动 AI 智能分析任务，支持同步和异步模式。异步模式下相同股票代码不允许重复提交。"
)
def trigger_analysis(
        request: AnalyzeRequest,
        config: Config = Depends(get_config_dep)
) -> Union[AnalysisResultResponse, JSONResponse]:
    """
    触发股票分析
    
    启动 AI 智能分析任务，支持单只或多只股票批量分析
    
    流程：
    1. 校验请求参数
    2. 异步模式：检查重复 -> 提交任务队列 -> 返回 202
    3. 同步模式：直接执行分析 -> 返回 200
    
    Args:
        request: 分析请求参数
        config: 配置依赖
        
    Returns:
        AnalysisResultResponse: 分析结果（同步模式）
        TaskAccepted | BatchTaskAcceptedResponse: 任务已接受（异步模式，返回 202）
        
    Raises:
        HTTPException: 400 - 请求参数错误
        HTTPException: 409 - 股票正在分析中
        HTTPException: 500 - 分析失败
    """
    # 校验请求参数
    stock_codes = []
    if request.stock_code:
        stock_codes.append(request.stock_code)
    if request.stock_codes:
        stock_codes.extend(request.stock_codes)

    if not stock_codes:
        raise api_error(400, "validation_error", "必须提供 stock_code 或 stock_codes 参数")

    # Normalize and de-duplicate inputs while preserving compatibility.
    resolved = [_resolve_and_normalize_input(c) for c in stock_codes]
    
    seen = set()
    unique_codes = []
    for code in resolved:
        if not code:
            continue
        # Use normalize_stock_code to ensure '600519' and '600519.SH' are merged
        norm = normalize_stock_code(code)
        if norm not in seen:
            seen.add(norm)
            unique_codes.append(code)
    
    stock_codes = unique_codes

    # Limit the number of stocks in a single request to prevent DoS
    MAX_BATCH_SIZE = 50
    if len(stock_codes) > MAX_BATCH_SIZE:
        raise api_error(400, "validation_error", f"单次分析请求最多支持 {MAX_BATCH_SIZE} 只股票")

    if not stock_codes:
        raise api_error(400, "validation_error", "股票代码不能为空或仅包含空白字符")

    # Sync mode only supports single-stock analysis.
    if not request.async_mode:
        if len(stock_codes) > 1:
            raise api_error(
                400,
                "validation_error",
                "同步模式仅支持单只股票分析，请使用 async_mode=true 进行批量分析",
            )
        return _handle_sync_analysis(stock_codes[0], request)

    # Async mode submits one task per stock.
    return _handle_async_analysis_batch(stock_codes, request)


def _handle_async_analysis_batch(
    stock_codes: list,
    request: AnalyzeRequest
) -> JSONResponse:
    """
    Handle asynchronous analysis requests, including batch submission.
    """
    task_queue = get_task_queue()
    
    # Preserve metadata for single-stock requests. For batch requests,
    # only carry through metadata that semantically applies to the whole
    # batch, such as import/image source tracking.
    is_single = len(stock_codes) == 1
    preserve_batch_metadata = request.selection_source in {"import", "image"}

    stock_name = request.stock_name if is_single else None
    original_query = request.original_query if (is_single or preserve_batch_metadata) else None
    selection_source = request.selection_source if (is_single or preserve_batch_metadata) else None
    notify = getattr(request, "notify", True)
    skills = getattr(request, "skills", None)
    analysis_phase = request.analysis_phase
    report_language = normalize_report_language(getattr(request, "report_language", None), default="")

    submit_kwargs = dict(
        stock_codes=stock_codes,
        stock_name=stock_name,
        original_query=original_query,
        selection_source=selection_source,
        report_type=request.report_type,
        analysis_phase=analysis_phase,
        force_refresh=request.force_refresh,
        notify=notify,
    )
    if report_language:
        submit_kwargs["report_language"] = report_language
    if skills is not None:
        submit_kwargs["skills"] = skills

    accepted_tasks, duplicate_errors = task_queue.submit_tasks_batch(**submit_kwargs)

    accepted = [
        BatchTaskAcceptedItem(
            task_id=task.task_id,
            trace_id=_get_task_trace_id(task),
            stock_code=task.stock_code,
            status="pending",
            message=f"分析任务已加入队列: {task.stock_code}",
            analysis_phase=task.analysis_phase,
        )
        for task in accepted_tasks
    ]
    duplicates = [
        BatchDuplicateTaskItem(
            stock_code=dup.stock_code,
            existing_task_id=dup.existing_task_id,
            message=str(dup),
        )
        for dup in duplicate_errors
    ]
    
    # 单只股票且被拒绝：保持 409 兼容性
    if len(stock_codes) == 1 and duplicates:
        dup = duplicates[0]
        error_response = DuplicateTaskErrorResponse(
            error="duplicate_task",
            message=dup.message,
            stock_code=dup.stock_code,
            existing_task_id=dup.existing_task_id,
        )
        return JSONResponse(
            status_code=409,
            content=error_response.model_dump()
        )
    
    # 单只股票成功：保持原有响应格式兼容性
    if len(stock_codes) == 1 and accepted:
        task_accepted = TaskAccepted(
            task_id=accepted[0].task_id,
            trace_id=accepted[0].trace_id,
            status="pending",
            message=accepted[0].message,
            analysis_phase=accepted[0].analysis_phase,
        )
        return JSONResponse(
            status_code=202,
            content=task_accepted.model_dump()
        )
    
    # 批量：返回汇总结果
    batch_response = BatchTaskAcceptedResponse(
        accepted=accepted,
        duplicates=duplicates,
        message=f"已提交 {len(accepted)} 个任务，{len(duplicates)} 个重复跳过",
    )
    return JSONResponse(
        status_code=202,
        content=batch_response.model_dump()
    )


def _handle_sync_analysis(
    stock_code: str,
    request: AnalyzeRequest
) -> AnalysisResultResponse:
    """
    处理同步分析请求
    
    直接执行分析，等待完成后返回结果
    """
    import uuid
    from src.services.analysis_service import AnalysisService
    
    query_id = uuid.uuid4().hex
    
    try:
        service = AnalysisService()
        result = service.analyze_stock(
            stock_code=stock_code,
            report_type=request.report_type,
            force_refresh=request.force_refresh,
            query_id=query_id,
            send_notification=getattr(request, "notify", True),
            skills=getattr(request, "skills", None),
            analysis_phase=request.analysis_phase,
            report_language=getattr(request, "report_language", None),
        )

        if result is None:
            error_message = service.last_error or f"分析股票 {stock_code} 失败"
            raise api_error(500, "analysis_failed", error_message)

        # 构建报告结构
        report_data = result.get("report", {})
        context_snapshot, fundamental_snapshot = _load_sync_fundamental_sources(
            query_id=query_id,
            stock_code=result.get("stock_code", stock_code),
        )
        report = _build_analysis_report(
            report_data,
            query_id,
            stock_code,
            result.get("stock_name"),
            context_snapshot=context_snapshot,
            fallback_fundamental_payload=fundamental_snapshot,
        )

        return AnalysisResultResponse(
            query_id=query_id,
            trace_id=result.get("trace_id") or query_id,
            stock_code=result.get("stock_code", stock_code),
            stock_name=result.get("stock_name"),
            report=report.model_dump() if report else None,
            diagnostic_summary=result.get("diagnostic_summary"),
            created_at=datetime.now().isoformat()
        )

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"分析失败: {e}", exc_info=True)
        raise api_error(500, "internal_error", f"分析过程发生错误: {str(e)}")


# ============================================================
# POST /market-review - 触发大盘复盘
# ============================================================

@router.post(
    "/market-review",
    response_model=MarketReviewAccepted,
    status_code=202,
    responses={
        202: {"description": "大盘复盘任务已接受", "model": MarketReviewAccepted},
        409: {"description": "大盘复盘正在执行", "model": ErrorResponse},
        500: {"description": "提交失败", "model": ErrorResponse},
    },
    summary="触发大盘复盘",
    description="提交一个后台大盘复盘任务，复用 CLI 的大盘复盘运行时装配并保存报告。该人工触发入口不按交易日检查跳过；接口内部仅提供进程内/单机防重，如多实例（多 Worker/多容器）部署，需结合外部幂等机制避免重复触发。",
)
def trigger_market_review(
    request: Optional[MarketReviewRequest] = Body(None),
    config: Config = Depends(get_config_dep),
) -> MarketReviewAccepted:
    """Trigger market review from Web/API without blocking the request."""
    request = request or MarketReviewRequest()

    runtime_config = _with_request_report_language(
        config,
        getattr(request, "report_language", None),
    )

    lock_token = _try_acquire_market_review_lock(runtime_config)
    if lock_token is None:
        raise api_error(409, "duplicate_market_review", "大盘复盘正在执行中，请稍后再试")

    try:
        task_id = uuid.uuid4().hex
        logger.info(
            "[MarketReview] component=market_review action=submit trigger_source=api "
            "task_id=%s region=%s send_notification=%s",
            task_id,
            getattr(runtime_config, "market_review_region", "cn") or "cn",
            request.send_notification,
        )
        task = get_task_queue().submit_background_task(
            lambda: _run_market_review_background(
                request.send_notification,
                override_region=None,
                lock_token=lock_token,
                config=runtime_config,
                query_id=task_id,
            ),
            stock_code="market_review",
            stock_name="大盘复盘",
            message="大盘复盘任务已提交",
            task_id=task_id,
        )
    except Exception:
        _release_market_review_lock(lock_token)
        raise

    return MarketReviewAccepted(
        status="accepted",
        message="大盘复盘任务已提交，完成后会保存报告并按配置推送通知",
        send_notification=request.send_notification,
        task_id=task.task_id,
        trace_id=_get_task_trace_id(task),
    )


# ============================================================
# GET /tasks - 获取任务列表
# ============================================================

@router.get(
    "/tasks",
    response_model=TaskListResponse,
    responses={
        200: {"description": "任务列表"},
    },
    summary="获取分析任务列表",
    description="获取当前所有分析任务，可按状态筛选"
)
def get_task_list(
    status: Optional[str] = Query(
        None,
        description="筛选状态：pending, processing, completed, failed, cancel_requested, cancelled（支持逗号分隔多个）"
    ),
    limit: int = Query(20, description="返回数量限制", ge=1, le=100),
) -> TaskListResponse:
    """
    获取分析任务列表
    
    Args:
        status: 状态筛选（可选）
        limit: 返回数量限制
        
    Returns:
        TaskListResponse: 任务列表响应
    """
    task_queue = get_task_queue()
    
    # 获取所有任务
    all_tasks = task_queue.list_all_tasks(limit=limit)
    
    # 状态筛选
    if status:
        status_list = [s.strip().lower() for s in status.split(",")]
        all_tasks = [t for t in all_tasks if t.status.value in status_list]
    
    # 统计信息
    stats = task_queue.get_task_stats()
    
    # 转换为 Schema
    task_infos = [
        TaskInfo(
            task_id=t.task_id,
            trace_id=_get_task_trace_id(t),
            stock_code=t.stock_code,
            stock_name=t.stock_name,
            status=t.status.value,
            progress=t.progress,
            message=t.message,
            report_type=t.report_type,
            created_at=t.created_at.isoformat(),
            started_at=t.started_at.isoformat() if t.started_at else None,
            completed_at=t.completed_at.isoformat() if t.completed_at else None,
            error=t.error,
            original_query=t.original_query,
            selection_source=t.selection_source,
            analysis_phase=t.analysis_phase,
            skills=getattr(t, "skills", None),
        )
        for t in all_tasks
    ]
    
    return TaskListResponse(
        total=stats["total"],
        pending=stats["pending"],
        processing=stats["processing"],
        tasks=task_infos,
    )


# ============================================================
# GET /tasks/stream - SSE 实时推送
# ============================================================

@router.get(
    "/tasks/stream",
    responses={
        200: {"description": "SSE 事件流", "content": {"text/event-stream": {}}},
    },
    summary="任务状态 SSE 流",
    description="通过 Server-Sent Events 实时推送任务状态变化"
)
async def task_stream():
    """
    SSE 任务状态流
    
    事件类型：
    - connected: 连接成功
    - task_created: 新任务创建
    - task_started: 任务开始执行
    - task_progress: 任务阶段进度更新
    - task_completed: 任务完成
    - task_failed: 任务失败
    - heartbeat: 心跳（每 30 秒）
    
    Returns:
        StreamingResponse: SSE 事件流
    """
    async def event_generator():
        task_queue = get_task_queue()
        event_queue: asyncio.Queue = asyncio.Queue()
        
        # 发送连接成功事件
        yield _format_sse_event("connected", {"message": "Connected to task stream"})
        
        # 发送当前进行中的任务
        pending_tasks = task_queue.list_pending_tasks()
        for task in pending_tasks:
            yield _format_sse_event("task_created", task.to_dict())
        
        # 订阅任务事件
        task_queue.subscribe(event_queue)
        
        try:
            while True:
                try:
                    # 等待事件，超时发送心跳
                    event = await asyncio.wait_for(event_queue.get(), timeout=30)
                    yield _format_sse_event(event["type"], event["data"])
                except asyncio.TimeoutError:
                    # 心跳
                    yield _format_sse_event("heartbeat", {
                        "timestamp": datetime.now().isoformat()
                    })
        except asyncio.CancelledError:
            logger.debug("SSE client disconnected, cancelling event generator")
            raise
        finally:
            task_queue.unsubscribe(event_queue)
    
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",  # 禁用 Nginx 缓冲
        }
    )


def _format_sse_event(event_type: str, data: Dict[str, Any]) -> str:
    """
    格式化 SSE 事件
    
    Args:
        event_type: 事件类型
        data: 事件数据
        
    Returns:
        SSE 格式字符串
    """
    return f"event: {event_type}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"


def _load_history_run_flow_by_query_id(
    query_id: str,
    *,
    code: Optional[str] = None,
    report_type: Optional[str] = None,
    fail_open: bool = False,
) -> Optional[RunFlowSnapshot]:
    try:
        from src.storage import DatabaseManager
        from src.services.history_service import HistoryService

        service = HistoryService(DatabaseManager.get_instance())
        return service.resolve_and_get_run_flow(
            query_id,
            code=code,
            report_type=report_type,
        )
    except Exception as e:
        if fail_open:
            logger.debug(
                "load history run-flow failed, falling back to task skeleton: query_id=%s err=%s",
                query_id,
                e,
            )
            return None
        raise


@router.get(
    "/tasks/{task_id}/flow",
    response_model=RunFlowSnapshot,
    responses={
        200: {"description": "任务运行流快照"},
        404: {"description": "任务不存在", "model": ErrorResponse},
        500: {"description": "服务器错误", "model": ErrorResponse},
    },
    summary="获取分析任务运行流",
    description="根据 task_id 查询任务数据流/信息流快照；活跃任务缺少诊断时返回骨架流。",
)
def get_task_run_flow(task_id: str) -> RunFlowSnapshot:
    """
    查询分析任务运行流。

    Active tasks are served from the in-memory task queue. Completed tasks try
    to hydrate from persisted history diagnostics using the same task_id/query_id.
    """
    task_queue = get_task_queue()
    task = task_queue.get_task(task_id)

    if task:
        if task.status == TaskStatusEnum.COMPLETED:
            task_report_type = _history_report_type_for_task_flow(
                getattr(task, "report_type", None)
            )
            task_stock_code = _safe_task_flow_text(getattr(task, "stock_code", None), max_length=32)
            if task_report_type == "market_review":
                task_stock_code = "MARKET"
            history_snapshot = _load_history_run_flow_by_query_id(
                task_id,
                code=task_stock_code,
                report_type=task_report_type,
                fail_open=True,
            )
            if history_snapshot is not None:
                return history_snapshot
        return build_task_run_flow_snapshot(task)

    try:
        history_snapshot = _load_history_run_flow_by_query_id(task_id)
        if history_snapshot is not None:
            return history_snapshot
    except Exception as e:
        logger.error(f"查询任务运行流失败: {e}", exc_info=True)
        raise api_error(500, "internal_error", f"查询任务运行流失败: {str(e)}")

    raise api_error(404, "not_found", f"任务 {task_id} 不存在或已过期")


def _safe_task_flow_text(value: Any, *, max_length: int) -> Optional[str]:
    if value is None:
        return None
    text = str(value).strip()
    if not text:
        return None
    return text[:max_length]


def _history_report_type_for_task_flow(value: Any) -> Optional[str]:
    text = _safe_task_flow_text(value, max_length=64)
    if text is None:
        return None
    normalized = text.lower().strip().replace("-", "_")
    aliases = {
        "detailed": "full",
        "simple": "simple",
        "full": "full",
        "brief": "brief",
        "market": "market_review",
        "market_review": "market_review",
    }
    return aliases.get(normalized, normalized)


def _datetime_to_iso(value: Any) -> Optional[str]:
    if isinstance(value, datetime):
        return value.isoformat()
    if isinstance(value, str) and value.strip():
        return value
    return None


def _extract_report_created_at(payload: Dict[str, Any]) -> Optional[str]:
    report = payload.get("report")
    if not isinstance(report, dict):
        return None

    meta = report.get("meta")
    if not isinstance(meta, dict):
        return None

    return _datetime_to_iso(meta.get("created_at"))


def _display_stock_code_from_index(stock_code: Any) -> str:
    code = str(stock_code or "").strip()
    if not code:
        return code
    return resolve_index_stock_code(code) or code


def _display_market_phase_summary(stock_code: Any, context_snapshot: Any) -> Any:
    return rebuild_market_phase_summary_for_stock_code(
        _display_stock_code_from_index(stock_code),
        context_snapshot,
    )


def _prepare_report_for_task_enrichment(
    report_data: Dict[str, Any],
    created_at: Optional[str],
) -> Dict[str, Any]:
    enriched_report = dict(report_data)
    meta = dict(enriched_report.get("meta") or {})
    if created_at and not _datetime_to_iso(meta.get("created_at")):
        meta["created_at"] = created_at
    enriched_report["meta"] = meta
    return enriched_report


def _ensure_report_action_fields(report_data: Dict[str, Any]) -> Dict[str, Any]:
    enriched_report = dict(report_data)
    meta = dict(enriched_report.get("meta") or {})
    summary = dict(enriched_report.get("summary") or {})
    details = enriched_report.get("details") if isinstance(enriched_report.get("details"), dict) else {}
    raw_result = details.get("raw_result") if isinstance(details.get("raw_result"), dict) else {}
    report_language = normalize_report_language(
        meta.get("report_language") or raw_result.get("report_language")
    )
    action_fields = build_action_fields(
        operation_advice=raw_result.get("operation_advice") or summary.get("operation_advice"),
        explicit_action=raw_result.get("action") or summary.get("action"),
        report_type=meta.get("report_type"),
        report_language=report_language,
    )
    summary["action"] = action_fields["action"]
    summary["action_label"] = action_fields["action_label"]
    enriched_report["summary"] = summary
    return enriched_report


def _build_task_analysis_result(task: Any) -> AnalysisResultResponse:
    """
    Normalize an in-memory completed task result to the public API contract.

    Older AnalysisService payloads contain stock_code/stock_name/report only.
    The status endpoint owns the task metadata, so it can supply the missing
    response fields without waiting for the database fallback path.
    """
    payload = dict(task.result)
    if not payload.get("query_id"):
        payload["query_id"] = task.task_id
    if not payload.get("trace_id"):
        payload["trace_id"] = _get_task_trace_id(task) or task.task_id
    if not payload.get("stock_code"):
        payload["stock_code"] = task.stock_code
    display_stock_code = _display_stock_code_from_index(payload.get("stock_code"))
    if display_stock_code:
        payload["stock_code"] = display_stock_code

    if not payload.get("stock_name") and getattr(task, "stock_name", None):
        payload["stock_name"] = task.stock_name

    if not payload.get("created_at"):
        payload["created_at"] = (
            _extract_report_created_at(payload)
            or _datetime_to_iso(getattr(task, "created_at", None))
            or _datetime_to_iso(getattr(task, "completed_at", None))
            or datetime.now().isoformat()
        )

    report_data = payload.get("report")
    stock_code = payload.get("stock_code")
    query_id = payload.get("query_id")
    report_enriched = False

    if isinstance(report_data, dict) and stock_code and query_id:
        context_snapshot, fundamental_snapshot = _load_sync_fundamental_sources(
            query_id=query_id,
            stock_code=stock_code,
        )
        if context_snapshot is not None or fundamental_snapshot is not None:
            try:
                report = _build_analysis_report(
                    _prepare_report_for_task_enrichment(
                        report_data,
                        payload.get("created_at"),
                    ),
                    query_id,
                    stock_code,
                    payload.get("stock_name") or getattr(task, "stock_name", None),
                    context_snapshot=context_snapshot,
                    fallback_fundamental_payload=fundamental_snapshot,
                )
                payload["report"] = report.model_dump()
                report_enriched = True
            except Exception as e:
                logger.debug(
                    "enrich in-memory task report failed (fail-open): task_id=%s err=%s",
                    getattr(task, "task_id", None),
                    e,
                )

    if not report_enriched and isinstance(report_data, dict):
        meta = report_data.get("meta")
        if isinstance(meta, dict) and display_stock_code:
            raw_meta_code = meta.get("stock_code") or getattr(task, "stock_code", None)
            meta["stock_code"] = display_stock_code
            meta["market_phase_summary"] = _display_market_phase_summary(
                raw_meta_code,
                {"market_phase_summary": meta.get("market_phase_summary")},
            )
        payload["report"] = _ensure_report_action_fields(report_data)

    return AnalysisResultResponse.model_validate(payload)


# ============================================================
# GET /status/{task_id} - 查询单个任务状态
# ============================================================

@router.get(
    "/status/{task_id}",
    response_model=TaskStatus,
    responses={
        200: {"description": "任务状态"},
        404: {"description": "任务不存在", "model": ErrorResponse},
    },
    summary="查询分析任务状态",
    description="根据 task_id 查询单个任务的状态"
)
def get_analysis_status(task_id: str) -> TaskStatus:
    """
    查询分析任务状态
    
    优先从任务队列查询，如果不存在则从数据库查询历史记录
    
    Args:
        task_id: 任务 ID
        
    Returns:
        TaskStatus: 任务状态信息
        
    Raises:
        HTTPException: 404 - 任务不存在
    """
    # 1. 先从任务队列查询
    task_queue = get_task_queue()
    task = task_queue.get_task(task_id)
    
    if task:
        result: Optional[AnalysisResultResponse] = None
        market_review_report = None
        market_review_payload = None

        if task.status == TaskStatusEnum.COMPLETED and isinstance(task.result, dict):
            if task.stock_code == "market_review":
                report_text = task.result.get("result")
                if isinstance(report_text, str) and report_text.strip():
                    market_review_report = report_text
                payload = task.result.get("market_review_payload")
                if isinstance(payload, dict):
                    market_review_payload = payload
            else:
                try:
                    result = _build_task_analysis_result(task)
                except Exception:
                    logger.warning(
                        "解析任务结果失败，回退为空返回: task_id=%s",
                        task.task_id,
                    )

        return TaskStatus(
            task_id=task.task_id,
            trace_id=_get_task_trace_id(task),
            status=task.status.value,
            progress=task.progress,
            result=result,
            market_review_report=market_review_report,
            market_review_payload=market_review_payload,
            error=task.error,
            stock_name=task.stock_name,
            original_query=task.original_query,
            selection_source=task.selection_source,
            analysis_phase=task.analysis_phase,
            skills=getattr(task, "skills", None),
        )
    
    # 2. 从数据库查询已完成的记录
    try:
        from src.storage import DatabaseManager
        db = DatabaseManager.get_instance()
        records = db.get_analysis_history(query_id=task_id, limit=1)

        if records:
            record = records[0]
            raw_result = parse_json_field(record.raw_result)
            if getattr(record, "report_type", None) == "market_review":
                market_review_report = None
                context_snapshot = parse_json_field(getattr(record, "context_snapshot", None))
                market_review_payload = None
                if isinstance(context_snapshot, dict):
                    payload = context_snapshot.get("market_review_payload")
                    if isinstance(payload, dict):
                        market_review_payload = payload
                if isinstance(raw_result, dict):
                    report_text = raw_result.get("raw_response") or raw_result.get("market_review_report")
                    if isinstance(report_text, str) and report_text.strip():
                        market_review_report = report_text
                if not market_review_report and record.news_content:
                    market_review_report = record.news_content

                return TaskStatus(
                    task_id=task_id,
                    trace_id=task_id,
                    status="completed",
                    progress=100,
                    result=None,
                    market_review_report=market_review_report,
                    market_review_payload=market_review_payload,
                    error=None,
                    stock_name=record.name,
                )

            model_used = normalize_model_used(
                (raw_result or {}).get("model_used") if isinstance(raw_result, dict) else None
            )
            report_language = normalize_report_language(
                (raw_result or {}).get("report_language") if isinstance(raw_result, dict) else None
            )
            stock_name = get_localized_stock_name(record.name, record.code, report_language)
            display_stock_code = _display_stock_code_from_index(record.code)

            # Extract current_price / change_pct from context_snapshot
            skills = None
            context_snapshot = parse_json_field(getattr(record, 'context_snapshot', None))
            analysis_context_pack_overview = extract_analysis_context_pack_overview(context_snapshot)
            market_phase_summary = _display_market_phase_summary(record.code, context_snapshot)
            api_context_snapshot = sanitize_context_snapshot_for_api(context_snapshot)
            if context_snapshot and isinstance(context_snapshot, dict):
                raw_skills = context_snapshot.get("skills")
                if isinstance(raw_skills, list):
                    skills = [str(skill) for skill in raw_skills]
            realtime_fields = extract_realtime_detail_fields(context_snapshot)
            current_price = realtime_fields.get("current_price")
            change_pct = realtime_fields.get("change_pct")
            fallback_fundamental = db.get_latest_fundamental_snapshot(
                query_id=task_id,
                code=record.code,
            )
            extracted_fundamental = extract_fundamental_detail_fields(
                context_snapshot=context_snapshot,
                fallback_fundamental_payload=fallback_fundamental,
            )
            extracted_boards = extract_board_detail_fields(
                context_snapshot=context_snapshot,
                fallback_fundamental_payload=fallback_fundamental,
            )
            has_board_details = (
                bool(extracted_boards.get("belong_boards"))
                or extracted_boards.get("sector_rankings") is not None
                or extracted_boards.get("concept_rankings") is not None
            )
            details = None
            if any(extracted_fundamental.values()) or has_board_details or context_snapshot is not None or analysis_context_pack_overview is not None:
                details = ReportDetails(
                    news_content=getattr(record, "news_content", None),
                    raw_result=raw_result,
                    context_snapshot=api_context_snapshot,
                    analysis_context_pack_overview=analysis_context_pack_overview,
                    financial_report=extracted_fundamental.get("financial_report"),
                    dividend_metrics=extracted_fundamental.get("dividend_metrics"),
                    belong_boards=extracted_boards.get("belong_boards"),
                    sector_rankings=extracted_boards.get("sector_rankings"),
                    concept_rankings=extracted_boards.get("concept_rankings"),
                )

            raw_dict = raw_result if isinstance(raw_result, dict) else {}
            action_fields = build_action_fields(
                operation_advice=raw_dict.get("operation_advice") or record.operation_advice,
                explicit_action=raw_dict.get("action"),
                report_type=getattr(record, 'report_type', None),
                report_language=report_language,
            )

            # Build report from DB record so completed tasks return real data
            report_dict = AnalysisReport(
                meta=ReportMeta(
                    id=record.id,
                    query_id=task_id,
                    stock_code=display_stock_code,
                    stock_name=stock_name,
                    report_type=getattr(record, 'report_type', None),
                    report_language=report_language,
                    created_at=record.created_at.isoformat() if record.created_at else None,
                    model_used=model_used,
                    current_price=current_price,
                    change_pct=change_pct,
                    market_phase_summary=market_phase_summary,
                ),
                summary=ReportSummary(
                    sentiment_score=record.sentiment_score,
                    operation_advice=record.operation_advice,
                    action=action_fields["action"],
                    action_label=action_fields["action_label"],
                    trend_prediction=record.trend_prediction,
                    analysis_summary=record.analysis_summary,
                ),
                strategy=ReportStrategy(
                    ideal_buy=_stringify_report_strategy_value(getattr(record, 'ideal_buy', None)),
                    secondary_buy=_stringify_report_strategy_value(getattr(record, 'secondary_buy', None)),
                    stop_loss=_stringify_report_strategy_value(getattr(record, 'stop_loss', None)),
                    take_profit=_stringify_report_strategy_value(getattr(record, 'take_profit', None)),
                ),
                details=details,
            ).model_dump()
            return TaskStatus(
                task_id=task_id,
                trace_id=task_id,
                status="completed",
                progress=100,
                result=AnalysisResultResponse(
                    query_id=task_id,
                    trace_id=task_id,
                    stock_code=display_stock_code,
                    stock_name=stock_name,
                    report=report_dict,
                    diagnostic_summary=build_run_diagnostic_summary(
                        context_snapshot=context_snapshot,
                        raw_result=raw_result,
                        report_saved=True,
                        query_id=task_id,
                        stock_code=display_stock_code,
                    ),
                    created_at=record.created_at.isoformat() if record.created_at else datetime.now().isoformat()
                ),
                error=None,
                skills=skills,
            )

    except Exception as e:
        logger.error(f"查询任务状态失败: {e}", exc_info=True)
        raise api_error(500, "internal_error", f"查询任务状态失败: {str(e)}")

    # 3. 任务不存在
    raise api_error(404, "not_found", f"任务 {task_id} 不存在或已过期")


# ============================================================
# 辅助函数
# ============================================================

def _load_sync_fundamental_sources(
    query_id: str,
    stock_code: str,
) -> tuple[Optional[Any], Optional[Dict[str, Any]]]:
    """
    Load context_snapshot and fallback fundamental snapshot for sync analyze response.
    """
    try:
        from src.storage import DatabaseManager

        db = DatabaseManager.get_instance()
        records = db.get_analysis_history(query_id=query_id, code=stock_code, limit=1)
        context_snapshot = None
        if records:
            context_snapshot = parse_json_field(getattr(records[0], "context_snapshot", None))

        fallback_fundamental = db.get_latest_fundamental_snapshot(
            query_id=query_id,
            code=stock_code,
        )
        return context_snapshot, fallback_fundamental
    except Exception as e:
        logger.debug(
            "load sync fundamental sources failed (fail-open): query_id=%s stock_code=%s err=%s",
            query_id,
            stock_code,
            e,
        )
        return None, None


def _stringify_report_strategy_value(value: Any) -> Optional[str]:
    if value is None:
        return None
    if isinstance(value, str):
        return value
    return str(value)


def _build_analysis_report(
        report_data: Dict[str, Any],
        query_id: str,
        stock_code: str,
        stock_name: Optional[str] = None,
        context_snapshot: Optional[Any] = None,
        fallback_fundamental_payload: Optional[Dict[str, Any]] = None,
) -> AnalysisReport:
    """
    构建符合 API 规范的分析报告
    
    Args:
        report_data: 原始报告数据
        query_id: 查询 ID
        stock_code: 股票代码
        stock_name: 股票名称
        context_snapshot: 上下文快照（可选）
        fallback_fundamental_payload: 基本面快照 payload（可选）
        
    Returns:
        AnalysisReport: 结构化的分析报告
    """
    meta_data = report_data.get("meta", {})
    summary_data = report_data.get("summary", {})
    strategy_data = report_data.get("strategy", {})
    details_data = report_data.get("details", {})
    report_language = normalize_report_language(
        meta_data.get("report_language")
        or (context_snapshot or {}).get("report_language")
        or getattr(Config.get_instance(), "report_language", "zh")
    )
    display_stock_code = _display_stock_code_from_index(meta_data.get("stock_code", stock_code))
    localized_stock_name = get_localized_stock_name(
        meta_data.get("stock_name", stock_name),
        display_stock_code,
        report_language,
    )
    realtime_fields = extract_realtime_detail_fields(context_snapshot)
    current_price = meta_data.get("current_price")
    if current_price is None:
        current_price = realtime_fields.get("current_price")
    change_pct = meta_data.get("change_pct")
    if change_pct is None:
        change_pct = realtime_fields.get("change_pct")
    raw_stock_code = meta_data.get("stock_code", stock_code)
    market_phase_summary = _display_market_phase_summary(raw_stock_code, context_snapshot)
    if market_phase_summary is None:
        meta_phase_summary = meta_data.get("market_phase_summary")
        if meta_phase_summary is not None:
            market_phase_summary = _display_market_phase_summary(
                raw_stock_code,
                {"market_phase_summary": meta_phase_summary},
            )

    meta = ReportMeta(
        query_id=meta_data.get("query_id", query_id),
        stock_code=display_stock_code,
        stock_name=localized_stock_name,
        report_type=meta_data.get("report_type", "detailed"),
        report_language=report_language,
        created_at=meta_data.get("created_at", datetime.now().isoformat()),
        current_price=current_price,
        change_pct=change_pct,
        model_used=normalize_model_used(meta_data.get("model_used")),
        market_phase_summary=market_phase_summary,
    )

    raw_result_data = details_data.get("raw_result") if isinstance(details_data.get("raw_result"), dict) else {}
    action_fields = build_action_fields(
        operation_advice=(
            raw_result_data.get("operation_advice")
            or details_data.get("operation_advice")
            or summary_data.get("operation_advice")
        ),
        explicit_action=raw_result_data.get("action") or details_data.get("action") or summary_data.get("action"),
        report_type=meta.report_type,
        report_language=report_language,
    )

    summary = ReportSummary(
        analysis_summary=summary_data.get("analysis_summary"),
        operation_advice=summary_data.get("operation_advice"),
        action=action_fields["action"],
        action_label=action_fields["action_label"],
        trend_prediction=summary_data.get("trend_prediction"),
        sentiment_score=summary_data.get("sentiment_score"),
        sentiment_label=summary_data.get("sentiment_label")
    )

    strategy = None
    if strategy_data:
        strategy = ReportStrategy(
            ideal_buy=_stringify_report_strategy_value(strategy_data.get("ideal_buy")),
            secondary_buy=_stringify_report_strategy_value(strategy_data.get("secondary_buy")),
            stop_loss=_stringify_report_strategy_value(strategy_data.get("stop_loss")),
            take_profit=_stringify_report_strategy_value(strategy_data.get("take_profit"))
        )

    extracted_fundamental = extract_fundamental_detail_fields(
        context_snapshot=context_snapshot,
        fallback_fundamental_payload=fallback_fundamental_payload,
    )
    extracted_boards = extract_board_detail_fields(
        context_snapshot=context_snapshot,
        fallback_fundamental_payload=fallback_fundamental_payload,
    )
    analysis_context_pack_overview = extract_analysis_context_pack_overview(context_snapshot)
    api_context_snapshot = sanitize_context_snapshot_for_api(context_snapshot)
    details = None
    has_board_details = (
        bool(extracted_boards.get("belong_boards"))
        or extracted_boards.get("sector_rankings") is not None
        or extracted_boards.get("concept_rankings") is not None
    )
    if details_data or any(extracted_fundamental.values()) or has_board_details or context_snapshot is not None or analysis_context_pack_overview is not None:
        details = ReportDetails(
            news_content=details_data.get("news_summary") or details_data.get("news_content"),
            raw_result=details_data,
            context_snapshot=api_context_snapshot,
            analysis_context_pack_overview=analysis_context_pack_overview,
            financial_report=extracted_fundamental.get("financial_report"),
            dividend_metrics=extracted_fundamental.get("dividend_metrics"),
            belong_boards=extracted_boards.get("belong_boards"),
            sector_rankings=extracted_boards.get("sector_rankings"),
            concept_rankings=extracted_boards.get("concept_rankings"),
        )

    return AnalysisReport(
        meta=meta,
        summary=summary,
        strategy=strategy,
        details=details
    )
