import json
import os
import re
import subprocess
import time
import uuid

from amc_server.context import (
    PROJECTS_DIR, SESSIONS_DIR, ZELLIJ_BIN, ZELLIJ_SESSION,
    _spawn_lock, _spawn_timestamps, SPAWN_COOLDOWN_SEC,
    validate_auth_token,
)
from amc_server.logging_utils import LOGGER

# Agent commands (AC-8, AC-9: full autonomous permissions)
AGENT_COMMANDS = {
    'claude': ['claude', '--dangerously-skip-permissions'],
    'codex': ['codex', '--dangerously-bypass-approvals-and-sandbox'],
}

# Module-level cache for projects list (AC-33)
_projects_cache: list[str] = []

# Characters unsafe for Zellij pane/tab names: control chars, quotes, backticks
_UNSAFE_PANE_CHARS = re.compile(r'[\x00-\x1f\x7f"\'`]')


def _sanitize_pane_name(name):
    """Sanitize a string for use as a Zellij pane name.

    Replaces control characters and quotes with underscores, collapses runs
    of whitespace into a single space, and truncates to 64 chars.
    """
    name = _UNSAFE_PANE_CHARS.sub('_', name)
    name = re.sub(r'\s+', ' ', name).strip()
    return name[:64] if name else 'unnamed'


def load_projects_cache():
    """Scan ~/projects/ and cache the list. Called on server start."""
    global _projects_cache
    try:
        projects = []
        for entry in PROJECTS_DIR.iterdir():
            if entry.is_dir() and not entry.name.startswith('.'):
                projects.append(entry.name)
        projects.sort()
        _projects_cache = projects
    except OSError:
        _projects_cache = []


class SpawnMixin:

    def _handle_spawn(self):
        """POST /api/spawn handler."""
        # Verify auth token (AC-38)
        auth_header = self.headers.get('Authorization', '')
        if not validate_auth_token(auth_header):
            self._send_json(401, {'ok': False, 'error': 'Unauthorized', 'code': 'UNAUTHORIZED'})
            return

        # Parse JSON body
        try:
            content_length = int(self.headers.get('Content-Length', 0))
            body = json.loads(self.rfile.read(content_length))
            if not isinstance(body, dict):
                self._json_error(400, 'Invalid JSON body')
                return
        except (json.JSONDecodeError, ValueError):
            self._json_error(400, 'Invalid JSON body')
            return

        project = body.get('project', '')
        agent_type = body.get('agent_type', '')

        # Validate params (returns resolved_path to avoid TOCTOU)
        validation = self._validate_spawn_params(project, agent_type)
        if 'error' in validation:
            self._send_json(400, {
                'ok': False,
                'error': validation['error'],
                'code': validation['code'],
            })
            return

        resolved_path = validation['resolved_path']
        spawn_id = str(uuid.uuid4())

        # Acquire _spawn_lock with 15s timeout
        acquired = _spawn_lock.acquire(timeout=15)
        if not acquired:
            self._send_json(503, {
                'ok': False,
                'error': 'Server busy - another spawn in progress',
                'code': 'SERVER_BUSY',
            })
            return

        try:
            # Check rate limit inside lock
            now = time.monotonic()
            last_spawn = _spawn_timestamps.get(project, 0)
            if now - last_spawn < SPAWN_COOLDOWN_SEC:
                remaining = SPAWN_COOLDOWN_SEC - (now - last_spawn)
                self._send_json(429, {
                    'ok': False,
                    'error': f'Rate limited - wait {remaining:.0f}s before spawning in {project}',
                    'code': 'RATE_LIMITED',
                })
                return

            # Execute spawn
            result = self._spawn_agent_in_project_tab(
                project, resolved_path, agent_type, spawn_id,
            )

            # Update timestamp only on success
            if result.get('ok'):
                _spawn_timestamps[project] = time.monotonic()

            status_code = 200 if result.get('ok') else 500
            result['spawn_id'] = spawn_id
            self._send_json(status_code, result)
        finally:
            _spawn_lock.release()

    def _validate_spawn_params(self, project, agent_type):
        """Validate spawn parameters. Returns resolved_path or error dict."""
        if not project or not isinstance(project, str):
            return {'error': 'Project name is required', 'code': 'MISSING_PROJECT'}

        # Reject whitespace-only names
        if not project.strip():
            return {'error': 'Project name is required', 'code': 'MISSING_PROJECT'}

        # Reject null bytes and control characters (U+0000-U+001F, U+007F)
        if '\x00' in project or re.search(r'[\x00-\x1f\x7f]', project):
            return {'error': 'Invalid project name', 'code': 'INVALID_PROJECT'}

        # Reject path traversal characters (/, \, ..)
        if '/' in project or '\\' in project or '..' in project:
            return {'error': 'Invalid project name', 'code': 'INVALID_PROJECT'}

        # Resolve symlinks and verify under PROJECTS_DIR
        candidate = PROJECTS_DIR / project
        try:
            resolved = candidate.resolve()
        except OSError:
            return {'error': f'Project not found: {project}', 'code': 'PROJECT_NOT_FOUND'}

        # Symlink escape check
        try:
            resolved.relative_to(PROJECTS_DIR.resolve())
        except ValueError:
            return {'error': 'Invalid project name', 'code': 'INVALID_PROJECT'}

        if not resolved.is_dir():
            return {'error': f'Project not found: {project}', 'code': 'PROJECT_NOT_FOUND'}

        if agent_type not in AGENT_COMMANDS:
            return {
                'error': f'Invalid agent type: {agent_type}. Must be one of: {", ".join(sorted(AGENT_COMMANDS))}',
                'code': 'INVALID_AGENT_TYPE',
            }

        return {'resolved_path': resolved}

    def _check_zellij_session_exists(self):
        """Check if the target Zellij session exists."""
        try:
            result = subprocess.run(
                [ZELLIJ_BIN, 'list-sessions'],
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode != 0:
                return False
            # Parse line-by-line to avoid substring false positives
            for line in result.stdout.splitlines():
                # Zellij outputs "session_name [Created ...]" or just "session_name"
                session_name = line.strip().split()[0] if line.strip() else ''
                if session_name == ZELLIJ_SESSION:
                    return True
            return False
        except FileNotFoundError:
            return False
        except subprocess.TimeoutExpired:
            return False
        except OSError:
            return False

    def _wait_for_session_file(self, spawn_id, timeout=10.0):
        """Poll for a session file matching spawn_id."""
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            try:
                for f in SESSIONS_DIR.glob('*.json'):
                    try:
                        data = json.loads(f.read_text())
                        if isinstance(data, dict) and data.get('spawn_id') == spawn_id:
                            return True
                    except (json.JSONDecodeError, OSError):
                        continue
            except OSError:
                pass
            time.sleep(0.25)
        return False

    def _spawn_agent_in_project_tab(self, project, project_path, agent_type, spawn_id):
        """Spawn an agent in a project-named Zellij tab."""
        # Check session exists
        if not self._check_zellij_session_exists():
            return {
                'ok': False,
                'error': f'Zellij session "{ZELLIJ_SESSION}" not found',
                'code': 'SESSION_NOT_FOUND',
            }

        # Create/switch to project tab
        try:
            result = subprocess.run(
                [
                    ZELLIJ_BIN, '--session', ZELLIJ_SESSION,
                    'action', 'go-to-tab-name', '--create', project,
                ],
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode != 0:
                return {
                    'ok': False,
                    'error': f'Failed to create tab: {result.stderr.strip() or "unknown error"}',
                    'code': 'TAB_ERROR',
                }
        except FileNotFoundError:
            return {'ok': False, 'error': f'Zellij not found at {ZELLIJ_BIN}', 'code': 'ZELLIJ_NOT_FOUND'}
        except subprocess.TimeoutExpired:
            return {'ok': False, 'error': 'Zellij tab creation timed out', 'code': 'TIMEOUT'}
        except OSError as e:
            return {'ok': False, 'error': str(e), 'code': 'SPAWN_ERROR'}

        # Build agent command
        agent_cmd = AGENT_COMMANDS[agent_type]
        pane_name = _sanitize_pane_name(f'{agent_type}-{project}')

        # Spawn pane with agent command
        env = os.environ.copy()
        env['AMC_SPAWN_ID'] = spawn_id

        try:
            result = subprocess.run(
                [
                    ZELLIJ_BIN, '--session', ZELLIJ_SESSION,
                    'action', 'new-pane',
                    '--name', pane_name,
                    '--cwd', str(project_path),
                    '--',
                ] + agent_cmd,
                env=env,
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode != 0:
                return {
                    'ok': False,
                    'error': f'Failed to spawn pane: {result.stderr.strip() or "unknown error"}',
                    'code': 'SPAWN_ERROR',
                }
        except FileNotFoundError:
            return {'ok': False, 'error': f'Zellij not found at {ZELLIJ_BIN}', 'code': 'ZELLIJ_NOT_FOUND'}
        except subprocess.TimeoutExpired:
            return {'ok': False, 'error': 'Pane spawn timed out', 'code': 'TIMEOUT'}
        except OSError as e:
            return {'ok': False, 'error': str(e), 'code': 'SPAWN_ERROR'}

        # Wait for session file to appear
        found = self._wait_for_session_file(spawn_id)
        if not found:
            LOGGER.warning(
                'Session file not found for spawn_id=%s after timeout (agent may still be starting)',
                spawn_id,
            )

        return {'ok': True, 'session_file_found': found}

    def _handle_projects(self):
        """GET /api/projects - return cached projects list."""
        self._send_json(200, {'ok': True, 'projects': list(_projects_cache)})

    def _handle_projects_refresh(self):
        """POST /api/projects/refresh - refresh and return projects list."""
        load_projects_cache()
        self._send_json(200, {'ok': True, 'projects': list(_projects_cache)})

    def _handle_health(self):
        """GET /api/health - check server and Zellij status."""
        zellij_ok = self._check_zellij_session_exists()
        self._send_json(200, {
            'ok': True,
            'zellij_session': ZELLIJ_SESSION,
            'zellij_available': zellij_ok,
        })
