from __future__ import annotations

"""把线上 ELK case 直接转换为 product-data canonical records。

这个脚本承接 online-mining-v2 和 product-data：
1. 按 request_id 拉主 NLP 表和前处理表。
2. 优先从前处理表 promptModel 的 [对话历史] 抽取模型真实输入 session。
3. 从用户指定 target 或线上 planning/code 结果推断标签。
4. 直接产出 canonical records，并可同步导出流通表格、训练 jsonl、评测 csv。
"""

import json
import re
import sys
from pathlib import Path
from typing import Any

from online_mining_common import (
    OnlineMiningError,
    compact_text,
    emit_error,
    emit_success,
    extract_case,
    fetch_one_by_request_id,
    load_json_payload,
    resolve_portable_path,
    string_values,
)

PRODUCT_DATA_SCRIPT_DIR = Path(__file__).resolve().parents[1].parent / "product-data" / "scripts"
sys.path.insert(0, str(PRODUCT_DATA_SCRIPT_DIR))

from product_data_portable import (  # noqa: E402
    canonical_target,
    export_dataset_records,
    export_planning_eval_csv,
    export_training_jsonl,
    record_id,
    target_type,
    validate_dataset_records,
)


def load_cases(payload: dict[str, Any]) -> list[dict[str, Any]]:
    if isinstance(payload.get("cases"), list):
        return [item for item in payload["cases"] if isinstance(item, dict)]
    cases_path = payload.get("cases_path")
    if cases_path:
        rows = []
        text = resolve_portable_path(str(cases_path)).read_text(encoding="utf-8")
        for line in text.splitlines():
            if not line.strip():
                continue
            item = json.loads(line)
            if isinstance(item, dict):
                rows.append(item)
        return rows
    return []


def merge_case(request_id: str, main_case: dict[str, Any] | None, pre_case: dict[str, Any] | None) -> dict[str, Any]:
    return {
        "request_id": request_id,
        "query": (pre_case or {}).get("query") or (main_case or {}).get("query"),
        "timestamp": (main_case or {}).get("timestamp") or (pre_case or {}).get("timestamp"),
        "session_id": (main_case or {}).get("session_id"),
        "device_id": (main_case or {}).get("device_id"),
        "device": (main_case or {}).get("device"),
        "tts": (main_case or {}).get("text"),
        "main": main_case,
        "pre_processing": pre_case,
    }


def fetch_cases_by_request_ids(
    request_ids: list[str],
    *,
    date: str | None,
    date_from: str | None = None,
    date_to: str | None = None,
    lookback_days: int | None = None,
) -> list[dict[str, Any]]:
    rows = []
    for request_id in request_ids:
        main_doc = fetch_one_by_request_id(
            "main",
            request_id,
            date,
            date_from=date_from,
            date_to=date_to,
            lookback_days=lookback_days,
        )
        pre_doc = fetch_one_by_request_id(
            "pre_processing",
            request_id,
            date,
            date_from=date_from,
            date_to=date_to,
            lookback_days=lookback_days,
        )
        main_case = extract_case("main", main_doc) if main_doc else None
        pre_case = extract_case("pre_processing", pre_doc) if pre_doc else None
        rows.append(merge_case(request_id, main_case, pre_case))
    return rows


def case_value(case: dict[str, Any], key: str) -> Any:
    if key in case:
        return case.get(key)
    main = case.get("main") if isinstance(case.get("main"), dict) else {}
    pre = case.get("pre_processing") if isinstance(case.get("pre_processing"), dict) else {}
    return case.get(key) or pre.get(key) or main.get(key)


def enrich_prev_session(case: dict[str, Any], *, limit: int) -> list[dict[str, Any]]:
    explicit = case.get("prev_session")
    if isinstance(explicit, list):
        return normalize_prev_session(explicit)[-limit:]
    timestamp = optional_int(case_value(case, "timestamp"))
    # 前处理 promptModel 是中控 planning 模型真实看到的输入，里面的 [对话历史]
    # 才是训练/评测应复现的 session。主表 session_id 不是同一概念，不默认使用。
    prompt_history = parse_prompt_history(str(case_value(case, "prompt_model") or ""))
    if prompt_history and timestamp is not None:
        step_ms = 60_000
        start_ts = timestamp - len(prompt_history) * step_ms
        for index, item in enumerate(prompt_history):
            item["timestamp"] = start_ts + index * step_ms
    return prompt_history[-limit:]


def parse_prompt_history(prompt: str) -> list[dict[str, Any]]:
    if not prompt:
        return []
    history_marker = "[对话历史]"
    current_marker = "[当前query]"
    history_start = prompt.rfind(history_marker)
    if history_start < 0:
        return []
    current_start = prompt.find(current_marker, history_start)
    if current_start < 0:
        return []
    block = prompt[history_start + len(history_marker) : current_start].strip()
    if not block:
        return []
    rows: list[dict[str, Any]] = []
    pending_user: str | None = None
    for raw_line in block.splitlines():
        line = raw_line.strip()
        if not line:
            continue
        user_match = re.match(r"^用户\s*[:：]\s*(.*)$", line)
        if user_match:
            if pending_user is not None:
                rows.append({"query": pending_user, "tts": "", "timestamp": 0})
            pending_user = user_match.group(1).strip()
            continue
        assistant_match = re.match(r"^小爱\s*[:：]\s*(.*)$", line)
        if assistant_match and pending_user is not None:
            rows.append({"query": pending_user, "tts": assistant_match.group(1).strip(), "timestamp": 0})
            pending_user = None
    if pending_user is not None:
        rows.append({"query": pending_user, "tts": "", "timestamp": 0})
    return [row for row in rows if row["query"]]


def normalize_prev_session(items: list[Any]) -> list[dict[str, Any]]:
    rows = []
    for item in items:
        if not isinstance(item, dict):
            continue
        query = str(item.get("query") or "").strip()
        if not query:
            continue
        rows.append(
            {
                "query": query,
                "tts": str(item.get("tts") or ""),
                "timestamp": optional_int(item.get("timestamp")) or 0,
            }
        )
    rows.sort(key=lambda item: int(item.get("timestamp") or 0))
    return rows


def infer_target(case: dict[str, Any], explicit_target: str) -> tuple[str, str]:
    if explicit_target.strip():
        return canonical_target(explicit_target.strip()), "input.target"
    case_target = str(case_value(case, "target") or "").strip()
    if case_target:
        return canonical_target(case_target), "case.target"

    pre = case.get("pre_processing") if isinstance(case.get("pre_processing"), dict) else {}
    main = case.get("main") if isinstance(case.get("main"), dict) else {}
    for key in ("code", "planning_result"):
        value = str(pre.get(key) or "").strip()
        if value:
            return canonical_target(value), f"pre_processing.{key}"

    llm_agent_info = main.get("llm_agent_info")
    if isinstance(llm_agent_info, dict):
        agent_type = str(llm_agent_info.get("agentType") or "").strip()
        if agent_type:
            return f'Agent(tag="{agent_type}")', "main.intention.intent_arbitrator_info.llm_agent_info.agentType"

    domain = str(main.get("domain") or case_value(case, "domain") or "").strip()
    if domain:
        if re.fullmatch(r"[A-Z][A-Za-z0-9_]*", domain):
            return f"{domain}()", "main.domain"
        return f'Agent(tag="{domain}")', "main.domain"
    raise OnlineMiningError("target is required because online logs do not contain code/planning_result/domain")


def case_to_record(
    case: dict[str, Any],
    *,
    dataset_label: str,
    target: str,
    complex_value: bool,
    index: int,
    session_limit: int,
) -> tuple[dict[str, Any], dict[str, Any]]:
    query = str(case_value(case, "query") or "").strip()
    if not query:
        raise OnlineMiningError(f"case {index} has no query")
    request_id = str(case_value(case, "request_id") or "").strip()
    if not request_id:
        raise OnlineMiningError(f"case {index} has no request_id")
    timestamp = optional_int(case_value(case, "timestamp"))
    if timestamp is None:
        raise OnlineMiningError(f"case {index} has no timestamp")

    final_target, target_source = infer_target(case, target)
    prev_session = enrich_prev_session(case, limit=session_limit)
    main = case.get("main") if isinstance(case.get("main"), dict) else {}
    pre = case.get("pre_processing") if isinstance(case.get("pre_processing"), dict) else {}
    record = {
        "record_id": record_id(source_type="online", batch_id="online", request_id=request_id, index=index),
        "source": {
            "type": "online",
            "request_id": request_id,
            "timestamp": timestamp,
        },
        "turn": {
            "query": query,
            "timestamp": timestamp,
        },
        "prev_session": prev_session,
        "context": {},
        "label": {
            "dataset_label": dataset_label,
            "target": final_target,
            "target_type": target_type(final_target),
        },
        "dimensions": {
            "complex": parse_complex(case_value(case, "complex"), default=complex_value),
        },
        "meta": {
            "session_id": str(case_value(case, "session_id") or ""),
            "device_id": str(case_value(case, "device_id") or ""),
            "device": compact_text(case_value(case, "device"), 500),
            "online_tts": str(case_value(case, "tts") or ""),
            "main_domain": str(main.get("domain") or ""),
            "main_func": str(main.get("func") or ""),
            "planning_result": str(pre.get("planning_result") or ""),
            "planning_code": str(pre.get("code") or ""),
            "hit_rules": pre.get("hit_rules") or main.get("hit_rules") or [],
            "candidate_domains": pre.get("candidate_domains") or [],
            "target_source": target_source,
        },
    }
    summary = {
        "request_id": request_id,
        "query": query,
        "target": final_target,
        "target_source": target_source,
        "prev_session_count": len(prev_session),
        "main_domain": record["meta"]["main_domain"],
        "planning_result": record["meta"]["planning_result"],
    }
    return record, summary


def optional_int(value: Any) -> int | None:
    if value is None or value == "":
        return None
    try:
        return int(value)
    except (TypeError, ValueError):
        return None


def parse_complex(value: Any, *, default: bool) -> bool:
    if value is None or value == "":
        return default
    if isinstance(value, bool):
        return value
    text = str(value).strip().lower()
    if text in {"true", "1", "yes", "y", "是", "复杂", "complex"}:
        return True
    if text in {"false", "0", "no", "n", "否", "不复杂", "简单", "simple"}:
        return False
    raise OnlineMiningError("complex must be true/false")


def write_records_jsonl(records: list[dict[str, Any]], path: str) -> str:
    output = resolve_portable_path(path)
    output.parent.mkdir(parents=True, exist_ok=True)
    with output.open("w", encoding="utf-8") as fh:
        for record in records:
            fh.write(json.dumps(record, ensure_ascii=False, separators=(",", ":")) + "\n")
    return str(output)


def main() -> int:
    try:
        payload = load_json_payload()
        dataset_label = str(payload.get("dataset_label") or "").strip()
        if not dataset_label:
            raise OnlineMiningError("dataset_label is required")
        target = str(payload.get("target") or "").strip()
        complex_value = parse_complex(payload.get("complex"), default=False)
        date = str(payload.get("date") or "").strip() or None
        date_from = str(payload.get("date_from") or "").strip() or None
        date_to = str(payload.get("date_to") or "").strip() or None
        lookback_days = payload.get("lookback_days")
        lookback_days = int(lookback_days) if lookback_days not in (None, "") else None
        session_limit = int(payload.get("session_limit") or 10)
        if session_limit < 0 or session_limit > 10:
            raise OnlineMiningError("session_limit must be between 0 and 10")

        request_ids = string_values(payload.get("request_ids"))
        cases = load_cases(payload)
        if request_ids:
            cases = fetch_cases_by_request_ids(
                request_ids,
                date=date,
                date_from=date_from,
                date_to=date_to,
                lookback_days=lookback_days,
            )
        if not cases:
            raise OnlineMiningError("request_ids, cases or cases_path is required")

        records = []
        summaries = []
        skipped = []
        for index, case in enumerate(cases, start=1):
            try:
                record, summary = case_to_record(
                    case,
                    dataset_label=dataset_label,
                    target=target,
                    complex_value=complex_value,
                    index=index,
                    session_limit=session_limit,
                )
            except Exception as exc:  # noqa: BLE001
                skipped.append({"index": index, "request_id": case.get("request_id"), "error": str(exc)})
                continue
            records.append(record)
            summaries.append(summary)

        if not records:
            raise OnlineMiningError(f"no records built; skipped={skipped}")

        records_path = write_records_jsonl(records, str(payload.get("records_output_path") or "scratchpad/online_records.jsonl"))
        validation = validate_dataset_records(records)
        output_dir = str(payload.get("output_dir") or "output")
        exports: dict[str, Any] = {}
        if bool(payload.get("export_records", True)):
            exports["records"] = export_dataset_records(
                records,
                output_path=str(resolve_portable_path(output_dir) / "records.jsonl"),
                output_format="jsonl",
                require_validation_ok=bool(payload.get("require_validation_ok", True)),
                overwrite=bool(payload.get("overwrite", True)),
                export_table=bool(payload.get("export_table", True)),
            )
        if bool(payload.get("export_training", False)):
            exports["training"] = export_training_jsonl(
                records,
                output_path=str(resolve_portable_path(output_dir) / "training.jsonl"),
                require_validation_ok=bool(payload.get("require_validation_ok", True)),
                overwrite=bool(payload.get("overwrite", True)),
            )
        if bool(payload.get("export_eval", False)):
            exports["eval"] = export_planning_eval_csv(
                records,
                output_path=str(resolve_portable_path(output_dir) / "eval_planning.csv"),
                require_validation_ok=bool(payload.get("require_validation_ok", True)),
                overwrite=bool(payload.get("overwrite", True)),
            )

        emit_success(
            {
                "date": date or (f"{date_from}..{date_to}" if date_from and date_to else f"past-{lookback_days}d" if lookback_days else "past-48h"),
                "record_count": len(records),
                "records_path": records_path,
                "validation": validation,
                "exports": exports,
                "summaries": summaries,
                "skipped": skipped,
            }
        )
        return 0
    except Exception as exc:  # noqa: BLE001
        emit_error(exc)
        return 1


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