#!/usr/bin/env python3
import argparse
import json
import os
import re
from typing import Any, Dict, List, Tuple

from dotenv import load_dotenv
from smolagents import InferenceClientModel, LiteLLMModel, ToolCallingAgent, tool


@tool
def detect_change_type(change_text: str) -> str:
    """배포 변경 요청 텍스트를 change type으로 분류한다.

    Args:
        change_text: 배포 변경 요청 원문
    """
    text = change_text.lower()

    rules = {
        "hotfix": ["긴급", "hotfix", "장애", "다운", "복구", "sev1", "critical"],
        "bugfix": ["버그", "오류", "수정", "fix", "패치"],
        "docs": ["문서", "가이드", "readme", "튜토리얼", "manual"],
    }

    for change_type, keywords in rules.items():
        if any(keyword in text for keyword in keywords):
            return change_type

    return "feature"


@tool
def assess_release_risk(change_type: str, users_affected: int = 0, payment_path: int = 0) -> str:
    """변경 유형/영향 범위를 기반으로 배포 리스크를 low/medium/high로 산정한다.

    Args:
        change_type: detect_change_type 결과(feature/bugfix/hotfix/docs)
        users_affected: 영향 사용자 수(정수)
        payment_path: 결제 경로 영향 여부(0/1)
    """
    c = change_type.strip().lower()
    users = int(users_affected)
    payment = int(payment_path)

    if payment == 1 and c in {"feature", "bugfix", "hotfix"}:
        return "high"

    if c == "hotfix" and users >= 1000:
        return "high"

    if c in {"feature", "bugfix", "hotfix"} and users >= 300:
        return "medium"

    if c == "docs":
        return "low"

    return "low"


@tool
def choose_test_plan(risk: str) -> str:
    """리스크 수준에 맞는 테스트 계획 코드를 반환한다.

    Args:
        risk: assess_release_risk 결과(low/medium/high)
    """
    r = risk.strip().lower()

    if r == "high":
        return "FULL_REGRESSION"
    if r == "medium":
        return "TARGETED_REGRESSION"
    return "SMOKE"


@tool
def choose_deploy_window(risk: str, change_type: str = "feature") -> str:
    """리스크/변경유형에 맞는 권장 배포 윈도우 코드를 반환한다.

    Args:
        risk: assess_release_risk 결과(low/medium/high)
        change_type: detect_change_type 결과
    """
    r = risk.strip().lower()
    c = change_type.strip().lower()

    if c == "hotfix" and r == "high":
        return "IMMEDIATE"
    if r == "high":
        return "WEEKEND"
    if r == "medium":
        return "OFF_HOURS"
    return "BUSINESS_HOURS"


def select_model(provider: str = "auto"):
    load_dotenv()

    openai_key = os.getenv("OPENAI_API_KEY")
    hf_token = os.getenv("HF_TOKEN")

    if provider in {"auto", "openai"} and openai_key:
        return LiteLLMModel(model_id="openai/gpt-4o-mini", api_key=openai_key), "openai/gpt-4o-mini"

    if provider in {"auto", "huggingface"} and hf_token:
        return InferenceClientModel(token=hf_token), "InferenceClientModel(default)"

    raise RuntimeError("모델 키를 찾지 못했습니다. OPENAI_API_KEY 또는 HF_TOKEN을 .env에 설정하세요.")


def build_task_prompt(change_text: str, users_affected: int, payment_path: int) -> str:
    return (
        "너는 릴리즈 승인 전 변경요청을 평가하는 운영 에이전트다.\\n"
        "아래 입력을 바탕으로 반드시 도구를 사용해 의사결정을 내려라.\\n"
        f"- change_text: {change_text}\\n"
        f"- users_affected: {users_affected}\\n"
        f"- payment_path: {payment_path}\\n\\n"
        "반드시 다음 순서로 도구를 호출해라: "
        "detect_change_type -> assess_release_risk -> choose_test_plan -> choose_deploy_window\\n"
        "마지막 줄은 정확히 이 형식으로만 출력해라:\\n"
        "FINAL: type=<type>; risk=<risk>; test=<test_plan>; window=<deploy_window>"
    )


def parse_final_line(result_text: str) -> Tuple[str, str, str, str]:
    pattern = re.compile(
        r"final\s*:\s*type\s*=\s*([a-z_]+)\s*;\s*risk\s*=\s*(low|medium|high)\s*;\s*test\s*=\s*([A-Z_]+)\s*;\s*window\s*=\s*([A-Z_]+)",
        re.IGNORECASE | re.DOTALL,
    )
    match = pattern.search(result_text)
    if not match:
        return "", "", "", ""

    change_type = match.group(1).lower()
    risk = match.group(2).lower()
    test_plan = match.group(3).upper()
    deploy_window = match.group(4).upper()
    return change_type, risk, test_plan, deploy_window


def run_selfcheck() -> Dict[str, Any]:
    checks = {
        "detect_change_type(긴급 결제 장애 hotfix)": detect_change_type("긴급 결제 장애 hotfix"),
        "assess_release_risk(hotfix,1200,1)": assess_release_risk("hotfix", 1200, 1),
        "choose_test_plan(medium)": choose_test_plan("medium"),
        "choose_deploy_window(low,docs)": choose_deploy_window("low", "docs"),
    }

    tools_ok = (
        checks["detect_change_type(긴급 결제 장애 hotfix)"] == "hotfix"
        and checks["assess_release_risk(hotfix,1200,1)"] == "high"
        and checks["choose_test_plan(medium)"] == "TARGETED_REGRESSION"
        and checks["choose_deploy_window(low,docs)"] == "BUSINESS_HOURS"
    )

    return {
        "mode": "selfcheck",
        "tools_ok": tools_ok,
        "checks": checks,
    }


def run_single(change_text: str, users_affected: int, payment_path: int, provider: str = "auto") -> Dict[str, Any]:
    model, model_name = select_model(provider)
    agent = ToolCallingAgent(
        tools=[detect_change_type, assess_release_risk, choose_test_plan, choose_deploy_window],
        model=model,
        max_steps=8,
    )

    task = build_task_prompt(change_text, users_affected, payment_path)
    result = str(agent.run(task))
    change_type, risk, test_plan, deploy_window = parse_final_line(result)

    return {
        "mode": "single",
        "model": model_name,
        "input": {
            "change_text": change_text,
            "users_affected": users_affected,
            "payment_path": payment_path,
        },
        "result": result,
        "parsed": {
            "type": change_type,
            "risk": risk,
            "test": test_plan,
            "window": deploy_window,
            "parse_ok": bool(change_type and risk and test_plan and deploy_window),
        },
    }


def run_eval(input_path: str, provider: str = "auto") -> Dict[str, Any]:
    with open(input_path, "r", encoding="utf-8") as f:
        tasks = json.load(f)

    if not isinstance(tasks, list):
        raise ValueError("입력 JSON 루트는 list여야 합니다")

    model, model_name = select_model(provider)
    agent = ToolCallingAgent(
        tools=[detect_change_type, assess_release_risk, choose_test_plan, choose_deploy_window],
        model=model,
        max_steps=8,
    )

    records: List[Dict[str, Any]] = []
    passed = 0

    for i, item in enumerate(tasks, start=1):
        change_text = str(item["change_text"])
        users_affected = int(item.get("users_affected", 0))
        payment_path = int(item.get("payment_path", 0))

        expected = item["expected"]
        exp_type = str(expected["type"]).lower()
        exp_risk = str(expected["risk"]).lower()
        exp_test = str(expected["test"]).upper()
        exp_window = str(expected["window"]).upper()

        task = build_task_prompt(change_text, users_affected, payment_path)
        result = str(agent.run(task))
        change_type, risk, test_plan, deploy_window = parse_final_line(result)

        ok = (
            change_type == exp_type
            and risk == exp_risk
            and test_plan == exp_test
            and deploy_window == exp_window
        )
        passed += int(ok)

        records.append(
            {
                "id": i,
                "input": {
                    "change_text": change_text,
                    "users_affected": users_affected,
                    "payment_path": payment_path,
                },
                "expected": {
                    "type": exp_type,
                    "risk": exp_risk,
                    "test": exp_test,
                    "window": exp_window,
                },
                "parsed": {
                    "type": change_type,
                    "risk": risk,
                    "test": test_plan,
                    "window": deploy_window,
                    "parse_ok": bool(change_type and risk and test_plan and deploy_window),
                },
                "pass": ok,
                "raw_result": result,
            }
        )

    total = len(records)
    score = round(passed / total, 3) if total else 0.0

    return {
        "mode": "eval",
        "model": model_name,
        "total": total,
        "passed": passed,
        "score": score,
        "pass": score >= 0.66,
        "records": records,
    }


def main():
    parser = argparse.ArgumentParser(description="HF Agents Day7 - Release gate mini project (hands-on)")
    parser.add_argument("--mode", choices=["selfcheck", "single", "eval"], default="selfcheck")
    parser.add_argument("--provider", choices=["auto", "openai", "huggingface"], default="auto")
    parser.add_argument("--change", default="결제 API 장애 긴급 hotfix 배포가 필요합니다.")
    parser.add_argument("--users", type=int, default=1200)
    parser.add_argument("--payment", type=int, default=1)
    parser.add_argument("--input", default="sample_tasks_day7.json")
    args = parser.parse_args()

    if args.mode == "selfcheck":
        output = run_selfcheck()
    elif args.mode == "single":
        output = run_single(args.change, args.users, args.payment, args.provider)
    else:
        output = run_eval(args.input, args.provider)

    print(json.dumps(output, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
