"""
preview_manager.py - 개발 서버 프리뷰 관리 도구

Thor (개발2팀 백엔드 담당) 작성
TDD: GREEN phase
"""

import argparse
import json
import os
import signal
import socket
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional

_WORKSPACE_ROOT = os.environ.get("WORKSPACE_ROOT", str(Path(__file__).resolve().parent.parent))
DEFAULT_CONFIG_PATH = str(Path(_WORKSPACE_ROOT) / "config" / "preview-ports.json")
DEFAULT_STATE_PATH = str(Path(_WORKSPACE_ROOT) / "memory" / "preview-state.json")
DEFAULT_PROJECTS_DIR = "/home/jay/projects"


class PreviewManager:
    """개발 서버 프리뷰 프로세스를 관리하는 클래스."""

    def __init__(
        self,
        config_path: str = DEFAULT_CONFIG_PATH,
        state_path: str = DEFAULT_STATE_PATH,
        projects_dir: str = DEFAULT_PROJECTS_DIR,
    ) -> None:
        self.config_path = Path(config_path)
        self.state_path = Path(state_path)
        self.projects_dir = Path(projects_dir)
        self.config = self._load_config()

    # ────────────────────────────────────────────────────────
    # 설정 / 상태 관리
    # ────────────────────────────────────────────────────────

    def _load_config(self) -> dict:
        """설정 파일(preview-ports.json)을 로드합니다."""
        if not self.config_path.exists():
            raise FileNotFoundError(f"설정 파일을 찾을 수 없습니다: {self.config_path}")
        with self.config_path.open("r", encoding="utf-8") as f:
            return json.load(f)

    def _save_config(self) -> None:
        """변경된 설정(next_port 등)을 파일에 저장합니다."""
        with self.config_path.open("w", encoding="utf-8") as f:
            json.dump(self.config, f, indent=2, ensure_ascii=False)
            f.write("\n")

    def load_state(self) -> dict:
        """상태 파일(preview-state.json)을 로드합니다. 파일이 없으면 빈 상태 반환."""
        if not self.state_path.exists():
            return {"previews": {}}
        with self.state_path.open("r", encoding="utf-8") as f:
            return json.load(f)

    def save_state(self, state: dict) -> None:
        """상태를 JSON 파일에 저장합니다."""
        self.state_path.parent.mkdir(parents=True, exist_ok=True)
        with self.state_path.open("w", encoding="utf-8") as f:
            json.dump(state, f, indent=2, ensure_ascii=False)
            f.write("\n")

    # ────────────────────────────────────────────────────────
    # 프로젝트 타입 감지
    # ────────────────────────────────────────────────────────

    def detect_project_type(self, project_dir: str) -> Optional[str]:
        """프로젝트 디렉토리를 분석하여 프로젝트 타입을 반환합니다.

        우선순위:
          1. manage.py → django
          2. package.json에 next → nextjs
          3. package.json에 vite → vite
          4. requirements.txt에 flask 또는 app.py → flask
          5. 미감지 → None
        """
        dir_path = Path(project_dir)

        # Django: manage.py 존재
        if (dir_path / "manage.py").exists():
            return "django"

        # Node.js 기반: package.json 파싱
        package_json_path = dir_path / "package.json"
        if package_json_path.exists():
            try:
                pkg = json.loads(package_json_path.read_text(encoding="utf-8"))
                all_deps: dict = {}
                all_deps.update(pkg.get("dependencies", {}))
                all_deps.update(pkg.get("devDependencies", {}))
                all_deps.update(pkg.get("peerDependencies", {}))

                if "next" in all_deps:
                    return "nextjs"
                if "vite" in all_deps:
                    return "vite"
            except (json.JSONDecodeError, OSError):
                pass

        # Flask: requirements.txt에 flask 포함
        req_path = dir_path / "requirements.txt"
        if req_path.exists():
            content = req_path.read_text(encoding="utf-8").lower()
            if "flask" in content:
                return "flask"

        # Flask: app.py 존재
        if (dir_path / "app.py").exists():
            return "flask"

        return None

    # ────────────────────────────────────────────────────────
    # 실행 명령어
    # ────────────────────────────────────────────────────────

    def get_start_command(self, project_type: str) -> list[str]:
        """프로젝트 타입에 따른 개발 서버 시작 명령어를 반환합니다."""
        commands: dict[str, list[str]] = {
            "nextjs": ["npm", "run", "dev"],
            "vite": ["npm", "run", "dev"],
            "flask": ["python3", "app.py"],
            "django": ["python3", "manage.py", "runserver"],
        }
        if project_type not in commands:
            raise ValueError(f"지원하지 않는 프로젝트 타입: {project_type}")
        return commands[project_type]

    # ────────────────────────────────────────────────────────
    # 포트 관리
    # ────────────────────────────────────────────────────────

    def get_port(self, project_name: str) -> int:
        """프로젝트에 할당된 포트를 반환합니다.

        설정에 있으면 설정 포트, 없으면 next_port를 할당하고 증가 후 저장.
        """
        assignments: dict[str, int] = self.config.get("port_assignments", {})
        if project_name in assignments:
            return assignments[project_name]

        # 신규 프로젝트: next_port 할당
        new_port: int = self.config["next_port"]
        self.config["port_assignments"][project_name] = new_port
        self.config["next_port"] = new_port + 1
        self._save_config()
        return new_port

    def check_port_available(self, port: int) -> bool:
        """포트 사용 가능 여부를 확인합니다. True = 사용 가능, False = 이미 사용 중."""
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.settimeout(1)
            result = sock.connect_ex(("127.0.0.1", port))
            return result != 0

    def get_preview_url(self, port: int) -> str:
        """프리뷰 URL을 반환합니다: http://<tailscale_ip>:<port>/"""
        tailscale_ip: str = self.config.get("tailscale_ip", "localhost")
        return f"http://{tailscale_ip}:{port}/"

    # ────────────────────────────────────────────────────────
    # 프로세스 관리
    # ────────────────────────────────────────────────────────

    def start(self, project_name: str) -> dict:
        """개발 서버를 시작하고 상태를 저장한 후 결과를 반환합니다."""
        project_dir = self.projects_dir / project_name
        if not project_dir.exists():
            raise FileNotFoundError(f"프로젝트 디렉토리를 찾을 수 없습니다: {project_dir}")

        project_type = self.detect_project_type(str(project_dir))
        if project_type is None:
            raise ValueError(f"프로젝트 타입을 감지할 수 없습니다: {project_dir}")

        port = self.get_port(project_name)

        if not self.check_port_available(port):
            raise RuntimeError(f"포트 {port}가 이미 사용 중입니다.")

        cmd = self.get_start_command(project_type)

        # Django runserver에 포트 추가
        if project_type == "django":
            cmd = cmd + [f"0.0.0.0:{port}"]

        # Flask/Next.js/Vite: 환경 변수로 포트 설정
        env = os.environ.copy()
        if project_type in ("nextjs", "vite"):
            env["PORT"] = str(port)
        elif project_type == "flask":
            env["FLASK_RUN_PORT"] = str(port)
            env["PORT"] = str(port)

        log_dir = Path(_WORKSPACE_ROOT) / "logs"
        log_dir.mkdir(parents=True, exist_ok=True)
        log_file = log_dir / f"preview-{project_name}.log"

        with log_file.open("a") as log_f:
            proc = subprocess.Popen(
                cmd,
                cwd=str(project_dir),
                env=env,
                stdout=log_f,
                stderr=log_f,
                start_new_session=True,
            )

        url = self.get_preview_url(port)
        started_at = datetime.now().isoformat(timespec="seconds")

        state = self.load_state()
        state["previews"][project_name] = {
            "port": port,
            "pid": proc.pid,
            "project_type": project_type,
            "url": url,
            "started_at": started_at,
            "project_dir": str(project_dir),
        }
        self.save_state(state)

        result = {
            "project_name": project_name,
            "port": port,
            "pid": proc.pid,
            "project_type": project_type,
            "url": url,
            "started_at": started_at,
        }
        print(f"[START] {project_name} → {url} (PID: {proc.pid})")
        return result

    def stop(self, project_name: str) -> dict:
        """개발 서버를 중지하고 상태에서 제거합니다."""
        state = self.load_state()
        previews: dict = state.get("previews", {})

        if project_name not in previews:
            raise KeyError(f"실행 중인 프리뷰가 없습니다: {project_name}")

        info = previews[project_name]
        pid: int = info["pid"]

        try:
            os.kill(pid, signal.SIGTERM)
            print(f"[STOP] {project_name} (PID: {pid}) 종료 신호 전송")
        except ProcessLookupError:
            print(f"[WARN] PID {pid}가 이미 종료된 상태입니다.")

        del state["previews"][project_name]
        self.save_state(state)

        return {"project_name": project_name, "pid": pid, "status": "stopped"}

    def status(self) -> list[dict]:
        """활성 프리뷰 목록을 반환합니다. PID 생존 여부를 확인합니다."""
        state = self.load_state()
        previews: dict = state.get("previews", {})
        active: list[dict] = []

        dead_projects: list[str] = []
        for name, info in previews.items():
            pid: int = info["pid"]
            alive = _is_pid_alive(pid)
            if alive:
                active.append({"project_name": name, **info, "alive": True})
            else:
                dead_projects.append(name)

        # 죽은 프로세스는 상태에서 제거
        if dead_projects:
            for name in dead_projects:
                del state["previews"][name]
            self.save_state(state)

        return active

    def list_projects(self) -> list[str]:
        """projects_dir 아래의 디렉토리 목록을 반환합니다."""
        if not self.projects_dir.exists():
            return []
        return sorted(entry.name for entry in self.projects_dir.iterdir() if entry.is_dir())


# ────────────────────────────────────────────────────────
# 헬퍼 함수
# ────────────────────────────────────────────────────────


def _is_pid_alive(pid: int) -> bool:
    """PID가 살아있는지 확인합니다."""
    try:
        os.kill(pid, 0)
        return True
    except (ProcessLookupError, PermissionError):
        return False


# ────────────────────────────────────────────────────────
# CLI
# ────────────────────────────────────────────────────────


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="preview_manager",
        description="개발 서버 프리뷰 관리 도구",
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    # start
    start_p = subparsers.add_parser("start", help="프리뷰 서버 시작")
    start_p.add_argument("name", help="프로젝트 이름")

    # stop
    stop_p = subparsers.add_parser("stop", help="프리뷰 서버 중지")
    stop_p.add_argument("name", help="프로젝트 이름")

    # status
    subparsers.add_parser("status", help="활성 프리뷰 목록 표시")

    # list
    subparsers.add_parser("list", help="프로젝트 디렉토리 목록 표시")

    return parser


def main(argv: Optional[list[str]] = None) -> None:
    parser = build_parser()
    args = parser.parse_args(argv)

    manager = PreviewManager()

    if args.command == "start":
        try:
            result = manager.start(args.name)
            print(f"URL: {result['url']}")
        except Exception as e:
            print(f"[ERROR] start 실패: {e}", file=sys.stderr)
            sys.exit(1)

    elif args.command == "stop":
        try:
            result = manager.stop(args.name)
            print(f"[STOP] {result['project_name']} 종료 완료")
        except Exception as e:
            print(f"[ERROR] stop 실패: {e}", file=sys.stderr)
            sys.exit(1)

    elif args.command == "status":
        active = manager.status()
        if not active:
            print("실행 중인 프리뷰가 없습니다.")
        else:
            print(f"{'프로젝트':<20} {'포트':>6} {'PID':>8} {'타입':<10} URL")
            print("-" * 70)
            for info in active:
                print(
                    f"{info['project_name']:<20} "
                    f"{info['port']:>6} "
                    f"{info['pid']:>8} "
                    f"{info['project_type']:<10} "
                    f"{info['url']}"
                )

    elif args.command == "list":
        projects = manager.list_projects()
        if not projects:
            print("프로젝트가 없습니다.")
        else:
            for p in projects:
                print(p)


if __name__ == "__main__":
    main()
