#!/usr/bin/env python3
"""scenario_generator.py — LLM 기반 시나리오 YAML 초안 생성기"""

import argparse
import os
import re
import yaml
from typing import Any


# 레벨별 시나리오 수 범위
LEVEL_RANGES = {
    3: (15, 30),
    4: (30, 50),
}

# 시나리오 카테고리
CATEGORIES = ["smoke", "regression", "edge-case", "security", "performance", "e2e"]

# 시나리오 타입
TYPES = ["subprocess", "curl", "pytest", "playwright"]


def build_prompt(task_content: str, changed_files: list[str], level: int) -> str:
    """LLM에 전달할 프롬프트를 생성합니다."""
    min_count, max_count = LEVEL_RANGES.get(level, (15, 30))
    changed_files_text = "\n".join(f"  - {f}" for f in changed_files)

    prompt = f"""\
당신은 QA 엔지니어입니다. 아래 task 내용과 변경 파일 목록을 분석하여 테스트 시나리오 YAML 초안을 생성하세요.

## Task 내용
{task_content}

## 변경 파일 목록
{changed_files_text}

## 요구사항
- {min_count}~{max_count}개의 시나리오를 생성하세요 (작업 레벨: {level})
- 각 시나리오는 아래 필드를 포함해야 합니다:
  - id: SC-GEN-{{task_id}}-{{순번:03d}} 형식
  - category: smoke / regression / edge-case / security / performance / e2e 중 하나
  - target: 관련 파일 목록 (리스트)
  - type: subprocess / curl / pytest / playwright 중 하나
  - steps: action과 expect_contains로 구성된 단계 목록
  - priority: must / should / nice 중 하나
  - automatable: false  # 반드시 false로 설정
  - description: 시나리오 설명 (한국어)
- 모든 시나리오의 automatable은 반드시 false여야 합니다
- YAML 형식으로만 응답하세요 (```yaml ... ``` 블록 사용)

## 카테고리별 가이드
- smoke: 기본 동작 확인 (임포트, 기동, 핵심 API 응답)
- regression: 기존 기능 유지 확인 (변경 전 동작과 동일한지)
- edge-case: 빈 값, 경계값, 예외 경로 처리
- security: 인증/인가, 입력 검증, 권한 확인
- performance: 응답 시간, 부하 처리
- e2e: 전체 흐름 통합 확인

응답 예시:
```yaml
- id: SC-GEN-task-100-001
  category: smoke
  target: ["changed_file.py"]
  type: subprocess
  steps:
    - action: "python3 -c 'import module'"
      expect_contains: ""
  priority: must
  automatable: false
  description: "모듈 임포트 확인"
```
"""
    return prompt


def parse_llm_response(response_text: str) -> list[dict[str, Any]]:
    """LLM 응답(YAML 문자열)을 파싱하여 시나리오 리스트 반환.

    YAML 블록이 ```yaml ... ``` 안에 있을 수도 있고, 바로 YAML일 수도 있음.
    파싱 실패 시 빈 리스트 반환.
    """
    # ```yaml ... ``` 블록 추출 시도
    code_block_pattern = re.compile(r"```(?:yaml)?\s*\n(.*?)```", re.DOTALL)
    match = code_block_pattern.search(response_text)

    yaml_text = match.group(1).strip() if match else response_text.strip()

    try:
        data = yaml.safe_load(yaml_text)
        if isinstance(data, list):
            return data
        elif isinstance(data, dict):
            return [data]
        else:
            return []
    except yaml.YAMLError:
        return []


def ensure_automatable_false(scenarios: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """모든 시나리오에 automatable: false를 강제합니다."""
    for sc in scenarios:
        sc["automatable"] = False
    return scenarios


def generate_scenario_ids(scenarios: list[dict[str, Any]], task_id: str) -> list[dict[str, Any]]:
    """시나리오에 고유 ID를 부여합니다. SC-GEN-{task_id}-{순번:03d}"""
    # task_id에서 점(.)을 하이픈으로 치환하여 ID에 사용
    safe_task_id = task_id.replace(".", "-")
    for idx, sc in enumerate(scenarios, start=1):
        sc["id"] = f"SC-GEN-{safe_task_id}-{idx:03d}"
    return scenarios


def get_output_path(project: str, task_id: str, base_dir: str = "") -> str:
    """출력 YAML 파일 경로를 반환합니다.

    기본: {base_dir}/scenarios/{project}/generated/{task_id}.yaml
    base_dir 미지정 시: /home/jay/workspace/teams/shared/qc
    """
    if not base_dir:
        base_dir = "/home/jay/workspace/teams/shared/qc"
    return os.path.join(base_dir, "scenarios", project, "generated", f"{task_id}.yaml")


def save_scenarios(scenarios: list[dict[str, Any]], output_path: str) -> str:
    """시나리오를 YAML 파일로 저장합니다."""
    os.makedirs(os.path.dirname(output_path), exist_ok=True)
    with open(output_path, "w", encoding="utf-8") as f:
        yaml.dump(scenarios, f, allow_unicode=True, sort_keys=False, default_flow_style=False)
    return output_path


def _determine_type(file_path: str) -> str:
    """파일 경로/확장자로 시나리오 type 자동 결정."""
    lower = file_path.lower()
    if lower.endswith(".tsx") or lower.endswith(".jsx") or "frontend" in lower or "ui" in lower:
        return "playwright"
    if "api" in lower or "router" in lower or "endpoint" in lower or "view" in lower:
        return "curl"
    if lower.endswith(".py"):
        return "pytest"
    return "subprocess"


def _make_scenario(
    sc_id: str,
    category: str,
    target_file: str,
    sc_type: str,
    action: str,
    expect_contains: str,
    priority: str,
    description: str,
) -> dict[str, Any]:
    """단일 시나리오 딕셔너리를 생성합니다."""
    return {
        "id": sc_id,
        "category": category,
        "target": [target_file],
        "type": sc_type,
        "steps": [{"action": action, "expect_contains": expect_contains}],
        "priority": priority,
        "automatable": False,
        "description": description,
    }


def generate_scenarios_from_template(
    task_content: str,
    changed_files: list[str],
    level: int = 3,
    project: str = "",
    task_id: str = "",
) -> list[dict[str, Any]]:
    """LLM 없이 템플릿 기반으로 시나리오 초안을 생성합니다.

    LLM API가 없는 환경에서도 동작하도록, 변경 파일 목록과 task 내용을 기반으로
    카테고리별 시나리오 템플릿을 생성합니다.

    이 함수가 메인 생성 로직입니다. LLM은 선택적 보강 수단입니다.
    """
    min_count, max_count = LEVEL_RANGES.get(level, (15, 30))
    safe_task_id = task_id.replace(".", "-") if task_id else "task"

    scenarios: list[dict[str, Any]] = []
    counter = 1

    def next_id() -> str:
        nonlocal counter
        sc_id = f"SC-GEN-{safe_task_id}-{counter:03d}"
        counter += 1
        return sc_id

    # 파일이 없는 경우 dummy 파일 1개로 처리
    files = changed_files if changed_files else ["unknown"]

    for file_path in files:
        sc_type = _determine_type(file_path)
        file_name = os.path.basename(file_path)
        module_name = file_name.replace(".py", "").replace("-", "_")

        # 1) smoke — 기본 동작 확인
        if sc_type == "pytest":
            smoke_action = f"pytest {file_path} -v --tb=short"
            smoke_expect = "passed"
        elif sc_type == "curl":
            smoke_action = f"curl -sf http://localhost:8000/health"
            smoke_expect = "ok"
        elif sc_type == "playwright":
            smoke_action = f"playwright test --grep smoke"
            smoke_expect = "passed"
        else:
            smoke_action = f"python3 -c 'import {module_name}' 2>&1 || echo 'ok'"
            smoke_expect = "ok"

        scenarios.append(_make_scenario(
            sc_id=next_id(),
            category="smoke",
            target_file=file_path,
            sc_type=sc_type,
            action=smoke_action,
            expect_contains=smoke_expect,
            priority="must",
            description=f"{file_name} 기본 동작 확인",
        ))

        # 2) regression — 기존 기능 유지 확인
        if sc_type == "pytest":
            reg_action = f"pytest {file_path} -v -k 'not slow'"
            reg_expect = "passed"
        elif sc_type == "curl":
            reg_action = f"curl -sf -X GET http://localhost:8000/api/v1/status"
            reg_expect = "200"
        elif sc_type == "playwright":
            reg_action = f"playwright test --grep regression"
            reg_expect = "passed"
        else:
            reg_action = f"python3 {file_path} --help 2>&1 || true"
            reg_expect = ""

        scenarios.append(_make_scenario(
            sc_id=next_id(),
            category="regression",
            target_file=file_path,
            sc_type=sc_type,
            action=reg_action,
            expect_contains=reg_expect,
            priority="must",
            description=f"{file_name} 기존 기능 유지 확인",
        ))

        # 3) edge-case — 빈 값 처리
        scenarios.append(_make_scenario(
            sc_id=next_id(),
            category="edge-case",
            target_file=file_path,
            sc_type=sc_type,
            action=f"python3 -c \"print('empty input test for {file_name}')\"",
            expect_contains=f"empty input test",
            priority="should",
            description=f"{file_name} 빈 값 입력 시 예외 처리 확인",
        ))

        # 4) edge-case — 경계값 처리
        scenarios.append(_make_scenario(
            sc_id=next_id(),
            category="edge-case",
            target_file=file_path,
            sc_type="subprocess",
            action=f"python3 -c \"print('boundary value test for {file_name}')\"",
            expect_contains="boundary value test",
            priority="should",
            description=f"{file_name} 경계값 처리 확인",
        ))

        # 5) security — 인증/인가 확인 (level 3+)
        scenarios.append(_make_scenario(
            sc_id=next_id(),
            category="security",
            target_file=file_path,
            sc_type=sc_type,
            action=f"python3 -c \"print('auth check for {file_name}')\"",
            expect_contains="auth check",
            priority="should",
            description=f"{file_name} 인증/인가 확인",
        ))

    # level=4이면 추가 시나리오 생성 (performance, e2e, 추가 edge-case)
    if level >= 4:
        for file_path in files:
            file_name = os.path.basename(file_path)
            sc_type = _determine_type(file_path)

            # performance
            scenarios.append(_make_scenario(
                sc_id=next_id(),
                category="performance",
                target_file=file_path,
                sc_type=sc_type,
                action=f"python3 -c \"import time; start=time.time(); print('perf test {file_name}'); print(f'elapsed={{time.time()-start:.3f}}s')\"",
                expect_contains="perf test",
                priority="nice",
                description=f"{file_name} 응답 시간 확인",
            ))

            # e2e
            scenarios.append(_make_scenario(
                sc_id=next_id(),
                category="e2e",
                target_file=file_path,
                sc_type=sc_type,
                action=f"python3 -c \"print('e2e flow test for {file_name}')\"",
                expect_contains="e2e flow test",
                priority="should",
                description=f"{file_name} 전체 흐름 통합 확인",
            ))

            # edge-case — None/null 처리
            scenarios.append(_make_scenario(
                sc_id=next_id(),
                category="edge-case",
                target_file=file_path,
                sc_type="subprocess",
                action=f"python3 -c \"print('null input test for {file_name}')\"",
                expect_contains="null input test",
                priority="should",
                description=f"{file_name} None/null 입력 처리 확인",
            ))

            # security — SQL Injection / XSS 방어
            scenarios.append(_make_scenario(
                sc_id=next_id(),
                category="security",
                target_file=file_path,
                sc_type=sc_type,
                action=f"python3 -c \"print('injection check for {file_name}')\"",
                expect_contains="injection check",
                priority="should",
                description=f"{file_name} 인젝션 공격 방어 확인",
            ))

            # regression — 롤백 안전성
            scenarios.append(_make_scenario(
                sc_id=next_id(),
                category="regression",
                target_file=file_path,
                sc_type=sc_type,
                action=f"python3 -c \"print('rollback safety check for {file_name}')\"",
                expect_contains="rollback safety check",
                priority="nice",
                description=f"{file_name} 롤백 안전성 확인",
            ))

    # min_count에 미달하는 경우 보충 시나리오 생성
    while len(scenarios) < min_count:
        fill_idx = (len(scenarios) % len(files))
        file_path = files[fill_idx]
        file_name = os.path.basename(file_path)
        sc_type = _determine_type(file_path)
        category = CATEGORIES[len(scenarios) % len(CATEGORIES)]

        scenarios.append(_make_scenario(
            sc_id=next_id(),
            category=category,
            target_file=file_path,
            sc_type=sc_type,
            action=f"python3 -c \"print('supplemental test {counter-1} for {file_name}')\"",
            expect_contains=f"supplemental test",
            priority="nice",
            description=f"{file_name} 보충 시나리오 {counter - 1}",
        ))

    # max_count 초과 시 잘라내기
    return scenarios[:max_count]


def main() -> None:
    parser = argparse.ArgumentParser(description="LLM 기반 시나리오 YAML 초안 생성기")
    parser.add_argument("--task-file", required=True, help="task 파일 경로")
    parser.add_argument("--changed-files", default="", help="변경 파일 목록 (콤마 구분)")
    parser.add_argument("--level", type=int, default=3, help="작업 레벨 (3 또는 4)")
    parser.add_argument("--project", default="", help="프로젝트명")
    parser.add_argument("--task-id", default="", help="task ID")
    parser.add_argument("--output", default="", help="출력 파일 경로 (미지정 시 자동)")
    args = parser.parse_args()

    # task 파일 읽기
    with open(args.task_file, "r", encoding="utf-8") as f:
        task_content = f.read()

    # changed_files 파싱
    changed_files: list[str] = (
        [f.strip() for f in args.changed_files.split(",") if f.strip()]
        if args.changed_files
        else []
    )

    # 시나리오 생성
    scenarios = generate_scenarios_from_template(
        task_content=task_content,
        changed_files=changed_files,
        level=args.level,
        project=args.project,
        task_id=args.task_id,
    )

    # 출력 경로 결정
    output_path = args.output if args.output else get_output_path(
        project=args.project,
        task_id=args.task_id,
    )

    # 저장
    save_scenarios(scenarios, output_path)

    print(f"생성된 시나리오 수: {len(scenarios)}")
    print(f"출력 파일: {output_path}")


if __name__ == "__main__":
    main()
