"""
Agent Update Distribution Endpoint.

Stable, frozen HTTP API — this contract NEVER changes shape.
  GET /api/v1/agent/version   →  {"version": "x.y.z"}
  GET /api/v1/agent/download  →  application/gzip  (agent-node tarball)

Auth: X-Agent-Token header must match the hub's secret key.
"""

import os
import io
import tarfile
import logging
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse, JSONResponse, Response

logger = logging.getLogger(__name__)

# Path to the agent-node source tree on the hub container.
# In Docker: mounted at /app/agent-node-source
# Overridable via env var for flexibility in other deployments.
_AGENT_NODE_DIR = os.environ.get(
    "AGENT_NODE_SRC_DIR",
    "/app/agent-node-source"
)
_SKILLS_DIR = os.environ.get(
    "SKILLS_SRC_DIR",
    "/app/skills"
)
_AGENT_NODE_DIR = os.path.abspath(_AGENT_NODE_DIR)
_SKILLS_DIR = os.path.abspath(_SKILLS_DIR)
_VERSION_FILE = os.path.join(_AGENT_NODE_DIR, "VERSION")

# Directories/files to exclude from the distributed tarball
_EXCLUDE_PATTERNS = {
    "__pycache__", ".git", "*.pyc", "*.pyo",
    "sync-node-1", "sync-node-2",  # Test workspace dirs
    "docker-compose.yml",           # Deployment-specific, not for generic clients
}


def _read_version() -> str:
    try:
        with open(_VERSION_FILE, "r") as f:
            return f.read().strip()
    except FileNotFoundError:
        return "0.0.0"


def _auth_ok(request: Request) -> bool:
    """Validates the agent auth token from the request header."""
    from app.config import settings
    token = request.headers.get("X-Agent-Token", "")
    if token == settings.SECRET_KEY:
        return True
    
    # M4 Fallback: Also allow any valid invite_token currently in the DB
    from app.db.session import SessionLocal
    from app.db.models import AgentNode
    db = SessionLocal()
    try:
        exists = db.query(AgentNode).filter(AgentNode.invite_token == token, AgentNode.is_active == True).first()
        return exists is not None
    finally:
        db.close()


def _should_exclude(path: str) -> bool:
    """Returns True if the path should be excluded from the tarball."""
    parts = path.replace("\\", "/").split("/")
    for part in parts:
        if part in _EXCLUDE_PATTERNS:
            return True
        if part.endswith(".pyc") or part.endswith(".pyo"):
            return True
    return False


def _build_tarball() -> bytes:
    """
    Builds an in-memory gzipped tarball of the agent-node and skills.
    The tarball is flat (no agent-node-source/ prefix) so extraction is direct.
    """
    buf = io.BytesIO()
    with tarfile.open(fileobj=buf, mode="w:gz") as tar:
        # Wrap everything in a top-level 'agent-node/' directory 
        # so bootstrap_installer can strip it consistently.
        ROOT_PREFIX = "agent-node"
        
        # 1. Add agent-node files
        if os.path.exists(_AGENT_NODE_DIR):
            for root, dirs, files in os.walk(_AGENT_NODE_DIR):
                dirs[:] = [d for d in dirs if not _should_exclude(os.path.join(root, d))]
                for filename in files:
                    abs_path = os.path.join(root, filename)
                    rel_path = os.path.join(ROOT_PREFIX, os.path.relpath(abs_path, _AGENT_NODE_DIR))
                    if not _should_exclude(rel_path):
                        tar.add(abs_path, arcname=rel_path)
        
        # 2. Add skills directory
        if os.path.exists(_SKILLS_DIR):
            for root, dirs, files in os.walk(_SKILLS_DIR):
                dirs[:] = [d for d in dirs if not _should_exclude(os.path.join(root, d))]
                for filename in files:
                    abs_path = os.path.join(root, filename)
                    rel_path = os.path.join(ROOT_PREFIX, "skills", os.path.relpath(abs_path, _SKILLS_DIR))
                    if not _should_exclude(rel_path):
                        tar.add(abs_path, arcname=rel_path)
    
    # 3. Flushed and complete
    return buf.getvalue()


def create_agent_update_router() -> APIRouter:
    router = APIRouter(prefix="/agent", tags=["Agent Update"])

    @router.get("/version", summary="Get current agent version")
    def get_agent_version(request: Request):
        """
        Returns the current agent-node version. Called by agent nodes at startup
        and periodically to detect if they need to self-update.
        """
        if not _auth_ok(request):
            raise HTTPException(status_code=401, detail="Unauthorized")

        version = _read_version()
        logger.info(f"[AgentUpdate] Version check → {version}")
        return JSONResponse({"version": version})

    @router.get("/download", summary="Download agent node tarball")
    def download_agent(request: Request):
        """
        Streams the current agent-node source as a gzipped tarball.
        Only called when an agent detects it is behind the hub's version.
        """
        if not _auth_ok(request):
            raise HTTPException(status_code=401, detail="Unauthorized")

        if not os.path.isdir(_AGENT_NODE_DIR):
            raise HTTPException(status_code=503, detail="Agent source not available on this hub.")

        version = _read_version()
        logger.info(f"[AgentUpdate] Serving agent tarball v{version}")

        try:
            tarball_bytes = _build_tarball()
        except Exception as e:
            logger.error(f"[AgentUpdate] Failed to build tarball: {e}")
            raise HTTPException(status_code=500, detail="Failed to package agent.")

        filename = f"cortex-agent-node-{version}.tar.gz"
        return StreamingResponse(
            iter([tarball_bytes]),
            media_type="application/gzip",
            headers={"Content-Disposition": f"attachment; filename={filename}"}
        )

    @router.get("/installer", summary="Download bootstrap_installer.py")
    def download_installer():
        """
        Returns the bootstrap_installer.py script itself.
        Used for initial one-liner provisioning on headless nodes.
        """
        installer_path = os.path.join(_AGENT_NODE_DIR, "bootstrap_installer.py")
        if not os.path.exists(installer_path):
            raise HTTPException(status_code=404, detail="Installer script not found on hub.")
        
        with open(installer_path, "rb") as f:
            content = f.read()
            
        return Response(
            content=content,
            media_type="text/x-python",
            headers={"Content-Disposition": "attachment; filename=bootstrap_installer.py"}
        )

    @router.get("/installer/ps1", summary="Download bootstrap_windows.ps1")
    def download_installer_ps1():
        """
        Returns the bootstrap_windows.ps1 script itself.
        Used for initial one-liner provisioning on Windows nodes.
        """
        installer_path = os.path.join(_AGENT_NODE_DIR, "bootstrap_windows.ps1")
        if not os.path.exists(installer_path):
            raise HTTPException(status_code=404, detail="Windows Installer script not found on hub.")
        
        with open(installer_path, "rb") as f:
            content = f.read()
            
        return Response(
            content=content,
            media_type="text/plain", # Use text/plain for easy piping to IEX
            headers={"Content-Disposition": "attachment; filename=bootstrap_windows.ps1"}
        )

    @router.get("/binary/{arch}", summary="Download Standalone Binary")
    def download_binary(arch: str):
        """
        Serves the All-in-One compiled binary for linux_amd64, linux_arm64, or darwin.
        """
        binary_path = os.path.join(_AGENT_NODE_DIR, "dist", arch, "cortex-agent")
        if not os.path.exists(binary_path):
            raise HTTPException(status_code=404, detail=f"Binary for architecture {arch} not found on hub.")
            
        with open(binary_path, "rb") as f:
            content = f.read()
            
        return Response(
            content=content,
            media_type="application/octet-stream",
            headers={"Content-Disposition": f"attachment; filename=cortex-agent-{arch}"}
        )

    return router
