#!/usr/bin/env python3
"""
Pending Actions Manager
- CLI와 모듈 import 양쪽으로 사용 가능
- fcntl.flock 기반 파일 잠금으로 동시 쓰기 충돌 방지
- Atomic write (임시 파일 → os.replace)
- JSON 읽기/쓰기 실패 시 .bak 백업 복구 로직
"""

import argparse
import fcntl
import json
import os
import shutil
import sys
import tempfile
from datetime import datetime, timezone
from typing import Optional

DEFAULT_ACTIONS_FILE = "/home/jay/workspace/memory/pending-actions.json"


class PendingActions:
    def __init__(self, actions_file: str = None):
        self.actions_file = actions_file or DEFAULT_ACTIONS_FILE
        self.backup_file = self.actions_file + ".bak"
        self._ensure_actions_file()

    # ------------------------------------------------------------------
    # 내부 유틸리티
    # ------------------------------------------------------------------

    def _ensure_actions_file(self):
        """액션 파일이 없으면 초기 구조로 생성한다."""
        os.makedirs(os.path.dirname(self.actions_file), exist_ok=True)
        if not os.path.exists(self.actions_file):
            self._write_data({"actions": [], "resolved": []})

    def _read_data(self) -> dict:
        """JSON 파일을 읽는다. 실패 시 백업에서 복구를 시도한다."""
        try:
            with open(self.actions_file, "r", encoding="utf-8") as f:
                fcntl.flock(f, fcntl.LOCK_SH)
                try:
                    content = f.read()
                    data = json.loads(content)
                finally:
                    fcntl.flock(f, fcntl.LOCK_UN)
            # 필수 키 보장
            data.setdefault("actions", [])
            data.setdefault("resolved", [])
            return data
        except (json.JSONDecodeError, OSError) as primary_err:
            # 백업 복구 시도
            if os.path.exists(self.backup_file):
                try:
                    with open(self.backup_file, "r", encoding="utf-8") as bf:
                        data = json.load(bf)
                    data.setdefault("actions", [])
                    data.setdefault("resolved", [])
                    # 복구된 데이터를 원본 위치에 저장
                    self._write_data(data)
                    return data
                except (json.JSONDecodeError, OSError):
                    pass
            raise RuntimeError(
                f"액션 파일 읽기 실패: {primary_err}. 백업도 사용 불가."
            )

    def _write_data(self, data: dict):
        """Atomic write: 임시 파일에 쓴 뒤 os.replace로 교체한다."""
        dir_path = os.path.dirname(self.actions_file)
        os.makedirs(dir_path, exist_ok=True)

        # 쓰기 전 백업 생성
        if os.path.exists(self.actions_file):
            try:
                shutil.copy2(self.actions_file, self.backup_file)
            except OSError:
                pass  # 백업 실패는 무시하고 진행

        # 임시 파일에 먼저 쓰기
        fd, tmp_path = tempfile.mkstemp(dir=dir_path, suffix=".tmp")
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as tf:
                fcntl.flock(tf, fcntl.LOCK_EX)
                try:
                    json.dump(data, tf, ensure_ascii=False, indent=2)
                    tf.flush()
                    os.fsync(tf.fileno())
                finally:
                    fcntl.flock(tf, fcntl.LOCK_UN)
            # Atomic rename
            os.replace(tmp_path, self.actions_file)
        except Exception:
            # 임시 파일 정리
            try:
                os.unlink(tmp_path)
            except OSError:
                pass
            raise

    def _next_action_id(self, data: dict) -> str:
        """actions + resolved 전체에서 최대 시퀀스 번호를 찾아 +1 반환."""
        max_seq = 0
        all_actions = data.get("actions", []) + data.get("resolved", [])
        for action in all_actions:
            aid = action.get("id", "")
            if aid.startswith("pa-"):
                try:
                    seq = int(aid[3:])
                    if seq > max_seq:
                        max_seq = seq
                except ValueError:
                    pass
        return f"pa-{max_seq + 1:03d}"

    # ------------------------------------------------------------------
    # 공개 API
    # ------------------------------------------------------------------

    def add_action(
        self,
        description: str,
        trigger_task_id: Optional[str] = None,
        trigger_type: str = "manual",
    ) -> dict:
        """새 약속/후속 작업을 추가하고 추가된 action dict를 반환한다."""
        lock_path = self.actions_file + ".lock"
        with open(lock_path, "w", encoding="utf-8") as lock_f:
            fcntl.flock(lock_f, fcntl.LOCK_EX)
            try:
                data = self._read_data()
                action_id = self._next_action_id(data)
                action = {
                    "id": action_id,
                    "description": description,
                    "trigger_task_id": trigger_task_id,
                    "trigger_type": trigger_type,
                    "status": "pending",
                    "created_at": datetime.now(timezone.utc).strftime(
                        "%Y-%m-%dT%H:%M:%S+00:00"
                    ),
                    "resolved_at": None,
                }
                data["actions"].append(action)
                self._write_data(data)
            finally:
                fcntl.flock(lock_f, fcntl.LOCK_UN)

        return action

    def resolve_action(self, action_id: str) -> Optional[dict]:
        """action_id 액션을 actions에서 제거하고 resolved로 이동한다."""
        lock_path = self.actions_file + ".lock"
        with open(lock_path, "w", encoding="utf-8") as lock_f:
            fcntl.flock(lock_f, fcntl.LOCK_EX)
            try:
                data = self._read_data()
                target = None
                new_actions = []
                for action in data["actions"]:
                    if action.get("id") == action_id:
                        target = action
                    else:
                        new_actions.append(action)

                if target is None:
                    return None

                target["status"] = "resolved"
                target["resolved_at"] = datetime.now(timezone.utc).strftime(
                    "%Y-%m-%dT%H:%M:%S+00:00"
                )
                data["actions"] = new_actions
                data["resolved"].append(target)
                self._write_data(data)
            finally:
                fcntl.flock(lock_f, fcntl.LOCK_UN)

        return target

    def get_pending(self) -> list:
        """pending 상태인 모든 action 리스트를 반환한다."""
        data = self._read_data()
        return [a for a in data["actions"] if a.get("status") == "pending"]

    def count_pending(self) -> int:
        """pending action 수를 반환한다."""
        return len(self.get_pending())

    def list_all(self, include_resolved: bool = False) -> dict:
        """전체 목록을 반환한다."""
        data = self._read_data()
        if include_resolved:
            return {
                "actions": data["actions"],
                "resolved": data["resolved"],
            }
        return {"actions": data["actions"]}


# ------------------------------------------------------------------
# CLI
# ------------------------------------------------------------------

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="pending-actions.py",
        description="Pending Actions Manager",
    )
    subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")

    # add
    p_add = subparsers.add_parser("add", help="새 약속/후속 작업을 추가한다")
    p_add.add_argument("--desc", required=True, help="약속/후속 작업 설명")
    p_add.add_argument("--trigger-task-id", default=None, dest="trigger_task_id", help="트리거 태스크 ID (예: task-123.1)")
    p_add.add_argument("--trigger-type", default="manual", dest="trigger_type", help="트리거 타입 (예: on_complete, manual)")
    p_add.add_argument("--actions-file", default=None, help="액션 파일 경로 (기본값 사용 가능)")

    # resolve
    p_resolve = subparsers.add_parser("resolve", help="액션을 resolved로 이동한다")
    p_resolve.add_argument("action_id", help="액션 ID (예: pa-001)")
    p_resolve.add_argument("--actions-file", default=None)

    # pending
    p_pending = subparsers.add_parser("pending", help="pending 상태 액션 목록을 JSON으로 출력한다")
    p_pending.add_argument("--actions-file", default=None)

    # list
    p_list = subparsers.add_parser("list", help="액션 목록을 조회한다")
    p_list.add_argument("--all", dest="all_actions", action="store_true", help="actions + resolved 모두")
    p_list.add_argument("--actions-file", default=None)

    # count
    p_cnt = subparsers.add_parser("count", help="pending 액션 수를 출력한다 (숫자만)")
    p_cnt.add_argument("--actions-file", default=None)

    return parser


def main():
    parser = build_parser()
    args = parser.parse_args()

    if args.command is None:
        parser.print_help()
        sys.exit(0)

    actions_file = getattr(args, "actions_file", None)
    pa = PendingActions(actions_file=actions_file)

    if args.command == "add":
        result = pa.add_action(
            description=args.desc,
            trigger_task_id=args.trigger_task_id,
            trigger_type=args.trigger_type,
        )
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif args.command == "resolve":
        result = pa.resolve_action(args.action_id)
        if result is None:
            print(
                json.dumps(
                    {"error": f"액션을 찾을 수 없습니다: {args.action_id}"},
                    ensure_ascii=False,
                    indent=2,
                )
            )
            sys.exit(1)
        else:
            print(json.dumps(result, ensure_ascii=False, indent=2))

    elif args.command == "pending":
        result = pa.get_pending()
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif args.command == "list":
        include_resolved = getattr(args, "all_actions", False)
        result = pa.list_all(include_resolved=include_resolved)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif args.command == "count":
        print(pa.count_pending())

    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()
