#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
glm-call.py - z.ai API (OpenAI 호환) 직접 호출 CLI 스크립트
"""

import argparse
import json
import os
import sys
import time

import requests

# ---------------------------------------------------------------------------
# System prompts
# ---------------------------------------------------------------------------

SYSTEM_PROMPTS = {
    "backend": (
        "당신은 시니어 백엔드 개발자입니다.\n"
        "전문 분야: Python, FastAPI, 데이터 모델링, REST API 설계, 데이터베이스, 보안.\n"
        "코딩 표준: black 포맷, isort 정렬, type hints 필수, docstring 작성.\n"
        "항상 에러 처리를 포함하고, 테스트 가능한 코드를 작성하세요."
    ),
    "frontend": (
        "당신은 시니어 프론트엔드 개발자입니다.\n"
        "전문 분야: React, TypeScript, Next.js, Tailwind CSS, 컴포넌트 설계, 상태 관리.\n"
        "코딩 표준: ESLint 준수, 접근성(a11y) 고려, 반응형 디자인.\n"
        "재사용 가능한 컴포넌트를 작성하고, 성능을 고려하세요."
    ),
    "uxui": (
        "당신은 시니어 UX/UI 설계자입니다.\n"
        "전문 분야: 와이어프레임, 정보 아키텍처, 사용성 테스트, 디자인 시스템, 접근성.\n"
        "사용자 중심 설계 원칙을 따르고, 명확한 설계 문서를 작성하세요.\n"
        "시각적 일관성과 사용 편의성을 최우선으로 고려하세요."
    ),
    "tester": (
        "당신은 시니어 QA 엔지니어입니다.\n"
        "전문 분야: pytest, 단위 테스트, 통합 테스트, 엣지 케이스 분석, 테스트 커버리지.\n"
        "코딩 표준: 테스트 함수명은 test_로 시작, arrange-act-assert 패턴, fixture 활용.\n"
        "버그 리포트는 재현 단계, 예상 결과, 실제 결과를 명시하세요."
    ),
    "general": (
        "당신은 범용 AI 어시스턴트입니다.\n"
        "요청된 작업을 정확하고 효율적으로 수행하세요.\n"
        "결과물은 명확하고 실용적이어야 합니다."
    ),
}

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

API_URL = "https://api.z.ai/api/coding/paas/v4/chat/completions"
ENV_KEYS_PATH = "/home/jay/workspace/.env.keys"
DEFAULT_MODEL = "glm-5"
DEFAULT_MAX_TOKENS = 8192
MAX_RETRIES = 2
RETRY_DELAY = 5  # seconds
REQUEST_TIMEOUT = 120  # seconds

VALID_MODELS = ["glm-5", "glm-4.7", "glm-4.7-flash", "glm-4.7-flashx"]


# ---------------------------------------------------------------------------
# API key loading
# ---------------------------------------------------------------------------


def _parse_env_keys_file(path: str) -> dict:
    """
    .env 형식의 키 파일을 직접 파싱하여 dict 로 반환합니다.
    따옴표(작은/큰따옴표) 제거, 주석(#) 및 빈 줄 무시.
    """
    result = {}
    try:
        with open(path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                if "=" not in line:
                    continue
                # export 접두어 제거
                if line.startswith("export "):
                    line = line[7:]
                key, _, raw_value = line.partition("=")
                key = key.strip()
                raw_value = raw_value.strip()
                # 따옴표 제거
                if (raw_value.startswith('"') and raw_value.endswith('"')) or (
                    raw_value.startswith("'") and raw_value.endswith("'")
                ):
                    raw_value = raw_value[1:-1]
                result[key] = raw_value
    except FileNotFoundError:
        pass
    except OSError as exc:
        print(f"[경고] .env.keys 파일 읽기 오류: {exc}", file=sys.stderr)
    return result


def load_api_key() -> str:
    """
    GLM_API_KEY 를 로드합니다.
    우선순위: 환경변수 > .env.keys 파일
    """
    # 1순위: 환경변수
    api_key = os.environ.get("GLM_API_KEY", "").strip()
    if api_key:
        return api_key

    # 2순위: .env.keys 파일
    env_vars = _parse_env_keys_file(ENV_KEYS_PATH)
    api_key = env_vars.get("GLM_API_KEY", "").strip()
    if api_key:
        return api_key

    print(
        "[오류] GLM_API_KEY 를 찾을 수 없습니다.\n"
        f"  환경변수 GLM_API_KEY 를 설정하거나\n"
        f'  {ENV_KEYS_PATH} 파일에 GLM_API_KEY="..." 형식으로 추가하세요.',
        file=sys.stderr,
    )
    sys.exit(1)


# ---------------------------------------------------------------------------
# API call
# ---------------------------------------------------------------------------


def call_api(
    api_key: str,
    model: str,
    system_prompt: str,
    user_message: str,
    max_tokens: int,
) -> str:
    """
    z.ai API 를 호출하고 assistant 응답 문자열을 반환합니다.
    최대 MAX_RETRIES 회 재시도하며, 실패 시 sys.exit(1).
    """
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    payload = {
        "model": model,
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_message},
        ],
        "max_tokens": max_tokens,
        "stream": False,
    }

    last_error = None
    for attempt in range(MAX_RETRIES + 1):
        if attempt > 0:
            print(
                f"[재시도] {attempt}/{MAX_RETRIES} ... {RETRY_DELAY}초 대기",
                file=sys.stderr,
            )
            time.sleep(RETRY_DELAY)

        try:
            response = requests.post(
                API_URL,
                headers=headers,
                json=payload,
                timeout=REQUEST_TIMEOUT,
            )
            response.raise_for_status()
            data = response.json()

            # OpenAI 호환 응답 파싱
            choices = data.get("choices", [])
            if not choices:
                raise ValueError(f"응답에 choices 가 없습니다: {data}")

            content = choices[0].get("message", {}).get("content", "")
            if content is None:
                content = ""
            return content

        except requests.exceptions.Timeout as exc:
            last_error = f"요청 타임아웃 ({REQUEST_TIMEOUT}s): {exc}"
            print(f"[오류] {last_error}", file=sys.stderr)

        except requests.exceptions.HTTPError as exc:
            status = exc.response.status_code if exc.response is not None else "?"
            body = ""
            if exc.response is not None:
                try:
                    body = exc.response.text
                except Exception:
                    pass
            last_error = f"HTTP {status} 오류: {exc}\n  응답 본문: {body}"
            print(f"[오류] {last_error}", file=sys.stderr)

        except requests.exceptions.RequestException as exc:
            last_error = f"네트워크 오류: {exc}"
            print(f"[오류] {last_error}", file=sys.stderr)

        except (ValueError, KeyError, json.JSONDecodeError) as exc:
            last_error = f"응답 파싱 오류: {exc}"
            print(f"[오류] {last_error}", file=sys.stderr)
            # 파싱 오류는 재시도해도 의미 없을 수 있으나 일관성을 위해 그대로 진행

    print(
        f"[오류] 최대 재시도 횟수({MAX_RETRIES})를 초과했습니다. 마지막 오류: {last_error}",
        file=sys.stderr,
    )
    sys.exit(1)


# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="glm-call",
        description="z.ai GLM API 직접 호출 CLI",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""예시:
  python3 glm-call.py --role backend --task "FastAPI 엔드포인트 구현"
  python3 glm-call.py --role backend --task-file /path/to/task.md --model glm-5 --output /path/to/output.md
  python3 glm-call.py --task "이 코드 리뷰해줘"
""",
    )

    # 입력
    input_group = parser.add_mutually_exclusive_group(required=True)
    input_group.add_argument(
        "--task",
        metavar="TEXT",
        help="인라인 텍스트 입력",
    )
    input_group.add_argument(
        "--task-file",
        metavar="FILE",
        dest="task_file",
        help="파일에서 태스크 읽기",
    )

    # 역할
    parser.add_argument(
        "--role",
        choices=list(SYSTEM_PROMPTS.keys()),
        default="general",
        help="역할 선택 (기본값: general)",
    )

    # 모델
    parser.add_argument(
        "--model",
        choices=VALID_MODELS,
        default=DEFAULT_MODEL,
        help=f"사용할 모델 (기본값: {DEFAULT_MODEL})",
    )

    # 토큰
    parser.add_argument(
        "--max-tokens",
        type=int,
        default=DEFAULT_MAX_TOKENS,
        dest="max_tokens",
        help=f"최대 토큰 수 (기본값: {DEFAULT_MAX_TOKENS})",
    )

    # 출력 파일
    parser.add_argument(
        "--output",
        metavar="FILE",
        help="결과를 저장할 파일 경로 (동시에 stdout 으로도 출력)",
    )

    return parser


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------


def main() -> None:
    parser = build_parser()
    args = parser.parse_args()

    # --- 태스크 문자열 결정 ---
    if args.task is not None:
        user_message = args.task
    else:
        task_path = args.task_file
        try:
            with open(task_path, "r", encoding="utf-8") as f:
                user_message = f.read()
        except FileNotFoundError:
            print(f"[오류] 파일을 찾을 수 없습니다: {task_path}", file=sys.stderr)
            sys.exit(1)
        except OSError as exc:
            print(f"[오류] 파일 읽기 실패: {exc}", file=sys.stderr)
            sys.exit(1)

    if not user_message.strip():
        print("[오류] 태스크 내용이 비어 있습니다.", file=sys.stderr)
        sys.exit(1)

    # --- API 키 로드 ---
    api_key = load_api_key()

    # --- System prompt 선택 ---
    system_prompt = SYSTEM_PROMPTS[args.role]

    # --- API 호출 ---
    result = call_api(
        api_key=api_key,
        model=args.model,
        system_prompt=system_prompt,
        user_message=user_message,
        max_tokens=args.max_tokens,
    )

    # --- 출력 ---
    # stdout (파이프 가능)
    sys.stdout.write(result)
    if result and not result.endswith("\n"):
        sys.stdout.write("\n")
    sys.stdout.flush()

    # 파일 저장 (--output 지정 시)
    if args.output:
        try:
            output_dir = os.path.dirname(args.output)
            if output_dir:
                os.makedirs(output_dir, exist_ok=True)
            with open(args.output, "w", encoding="utf-8") as f:
                f.write(result)
                if result and not result.endswith("\n"):
                    f.write("\n")
            print(f"[저장] {args.output}", file=sys.stderr)
        except OSError as exc:
            print(f"[오류] 파일 저장 실패: {exc}", file=sys.stderr)
            sys.exit(1)


if __name__ == "__main__":
    main()
