"""~/workspace/ project control: list, run, stop, delete, logs."""
import json
import os
import shutil
import signal
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from urllib.parse import parse_qs, urlparse

import constants

from handlers._validators import validate_slug


class WorkspaceHandlers:
    """Mixin for /api/workspace and /api/workspace/*."""

    def _validate_project(self, name):
        """Validate project name and return its path, or None."""
        if validate_slug(name) is not None:
            return None
        project_dir = constants.WORKSPACE_DIR / name
        if not project_dir.exists() or not project_dir.is_dir():
            return None
        if not str(project_dir.resolve()).startswith(str(constants.WORKSPACE_DIR)):
            return None
        return project_dir

    # ── /api/workspace list ─────────────────────────────────────
    def handle_workspace_list(self):
        """List projects in ~/workspace/ with Ralph status, git info, etc."""
        try:
            projects = []
            if not constants.WORKSPACE_DIR.exists():
                self.send_json({'projects': []})
                return

            for project_dir in sorted(constants.WORKSPACE_DIR.iterdir()):
                if not project_dir.is_dir() or project_dir.name.startswith('.'):
                    continue

                ralph_dir = project_dir / 'scripts' / 'ralph'
                prd_json = ralph_dir / 'prd.json'
                tasks_dir = project_dir / 'tasks'

                proj = {
                    'name': project_dir.name,
                    'path': str(project_dir),
                    'hasRalph': ralph_dir.exists(),
                    'hasPrd': any(tasks_dir.glob('prd-*.md')) if tasks_dir.exists() else False,
                    'hasMain': (project_dir / 'main.py').exists(),
                    'hasVenv': (project_dir / 'venv').exists(),
                    'hasReadme': (project_dir / 'README.md').exists(),
                    'ralph': None,
                    'process': {'running': False, 'pid': None, 'port': None},
                    'git': None,
                }

                # Ralph status
                if prd_json.exists():
                    try:
                        prd = json.loads(prd_json.read_text())
                        stories = prd.get('userStories', [])
                        complete = sum(1 for s in stories if s.get('passes'))

                        ralph_pid = None
                        ralph_running = False
                        pid_file = ralph_dir / '.ralph.pid'
                        if pid_file.exists():
                            try:
                                pid = int(pid_file.read_text().strip())
                                os.kill(pid, 0)
                                ralph_running = True
                                ralph_pid = pid
                            except (ValueError, ProcessLookupError, PermissionError):
                                pass

                        last_iter = None
                        tech = {}
                        logs_dir = ralph_dir / 'logs'
                        if logs_dir.exists():
                            log_files = sorted(logs_dir.glob('iteration-*.log'), key=lambda f: f.stat().st_mtime, reverse=True)
                            if log_files:
                                mtime = log_files[0].stat().st_mtime
                                last_iter = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M')
                                tech = prd.get('techStack', {})

                        proj['ralph'] = {
                            'running': ralph_running,
                            'pid': ralph_pid,
                            'storiesTotal': len(stories),
                            'storiesComplete': complete,
                            'lastIteration': last_iter,
                            'stories': [
                                {'id': s.get('id', ''), 'title': s.get('title', ''), 'passes': s.get('passes', False)}
                                for s in stories
                            ],
                        }
                        proj['techStack'] = {
                            'type': tech.get('type', ''),
                            'commands': tech.get('commands', {}),
                            'port': tech.get('port'),
                        }
                    except (json.JSONDecodeError, IOError):
                        pass

                # Check if main.py is running
                if proj['hasMain']:
                    try:
                        result = subprocess.run(
                            ['pgrep', '-f', f'python.*{project_dir.name}/main.py'],
                            capture_output=True, text=True, timeout=3,
                        )
                        if result.stdout.strip():
                            pids = result.stdout.strip().split('\n')
                            port = None
                            if prd_json.exists():
                                try:
                                    prd_data = json.loads(prd_json.read_text())
                                    port = prd_data.get('techStack', {}).get('port')
                                except (json.JSONDecodeError, IOError):
                                    pass
                            proj['process'] = {
                                'running': True,
                                'pid': int(pids[0]),
                                'port': port,
                            }
                    except Exception:
                        pass

                # Git info (using _run_git from GitHandlers mixin)
                if (project_dir / '.git').exists():
                    try:
                        branch = self._run_git(project_dir, ['branch', '--show-current']).stdout.strip()
                        last_commit = self._run_git(project_dir, ['log', '-1', '--format=%h - %s']).stdout.strip()
                        status_out = self._run_git(project_dir, ['status', '--short']).stdout.strip()
                        uncommitted = len([l for l in status_out.split('\n') if l.strip()]) if status_out else 0
                        proj['git'] = {
                            'branch': branch,
                            'lastCommit': last_commit,
                            'uncommitted': uncommitted,
                        }
                    except Exception:
                        pass

                projects.append(proj)

            self.send_json({'projects': projects})
        except Exception as e:
            self.send_json({'error': str(e)}, 500)

    # ── /api/workspace/run  (main | ralph | test) ───────────────
    def handle_workspace_run(self):
        """Start a project process (main.py, ralph.sh, or pytest)."""
        try:
            data = self._read_post_json()
            project_name = data.get('project', '')
            command = data.get('command', '')

            project_dir = self._validate_project(project_name)
            if not project_dir:
                self.send_json({'success': False, 'error': 'Invalid project'}, 400)
                return

            allowed_commands = {'main', 'ralph', 'test'}
            if command not in allowed_commands:
                self.send_json({'success': False, 'error': f'Invalid command. Allowed: {", ".join(allowed_commands)}'}, 400)
                return

            ralph_dir = project_dir / 'scripts' / 'ralph'

            if command == 'main':
                main_py = project_dir / 'main.py'
                if not main_py.exists():
                    self.send_json({'success': False, 'error': 'No main.py found'}, 404)
                    return

                venv_python = project_dir / 'venv' / 'bin' / 'python'
                python_cmd = str(venv_python) if venv_python.exists() else sys.executable

                log_path = ralph_dir / 'logs' / 'main.log' if ralph_dir.exists() else project_dir / 'main.log'
                log_path.parent.mkdir(parents=True, exist_ok=True)

                with open(log_path, 'a') as log_file:
                    proc = subprocess.Popen(
                        [python_cmd, 'main.py'],
                        cwd=str(project_dir),
                        stdout=log_file,
                        stderr=log_file,
                        start_new_session=True,
                    )
                self.send_json({'success': True, 'pid': proc.pid, 'log': str(log_path)})

            elif command == 'ralph':
                ralph_sh = ralph_dir / 'ralph.sh'
                if not ralph_sh.exists():
                    self.send_json({'success': False, 'error': 'No ralph.sh found'}, 404)
                    return

                log_path = ralph_dir / 'logs' / 'ralph.log'
                log_path.parent.mkdir(parents=True, exist_ok=True)

                with open(log_path, 'a') as log_file:
                    proc = subprocess.Popen(
                        ['bash', str(ralph_sh)],
                        cwd=str(project_dir),
                        stdout=log_file,
                        stderr=log_file,
                        start_new_session=True,
                    )

                (ralph_dir / '.ralph.pid').write_text(str(proc.pid))
                self.send_json({'success': True, 'pid': proc.pid, 'log': str(log_path)})

            elif command == 'test':
                venv_python = project_dir / 'venv' / 'bin' / 'python'
                python_cmd = str(venv_python) if venv_python.exists() else sys.executable

                result = subprocess.run(
                    [python_cmd, '-m', 'pytest', '-v', '--tb=short'],
                    cwd=str(project_dir),
                    capture_output=True, text=True,
                    timeout=120,
                )
                self.send_json({
                    'success': result.returncode == 0,
                    'output': result.stdout + result.stderr,
                    'returncode': result.returncode,
                })

        except subprocess.TimeoutExpired:
            self.send_json({'success': False, 'error': 'Test timeout (120s)'}, 500)
        except Exception as e:
            self.send_json({'success': False, 'error': str(e)}, 500)

    def handle_workspace_stop(self):
        """Stop a project process."""
        try:
            data = self._read_post_json()
            project_name = data.get('project', '')
            target = data.get('target', '')

            project_dir = self._validate_project(project_name)
            if not project_dir:
                self.send_json({'success': False, 'error': 'Invalid project'}, 400)
                return

            if target not in ('main', 'ralph'):
                self.send_json({'success': False, 'error': 'Invalid target. Use: main, ralph'}, 400)
                return

            if target == 'ralph':
                pid_file = project_dir / 'scripts' / 'ralph' / '.ralph.pid'
                if pid_file.exists():
                    try:
                        pid = int(pid_file.read_text().strip())
                        proc_cwd = Path(f'/proc/{pid}/cwd').resolve()
                        if str(proc_cwd).startswith(str(constants.WORKSPACE_DIR)):
                            os.killpg(os.getpgid(pid), signal.SIGTERM)
                            self.send_json({'success': True, 'message': f'Ralph stopped (PID {pid})'})
                        else:
                            self.send_json({'success': False, 'error': 'Process not in workspace'}, 403)
                    except ProcessLookupError:
                        self.send_json({'success': True, 'message': 'Process already stopped'})
                    except PermissionError:
                        self.send_json({'success': False, 'error': 'Permission denied'}, 403)
                else:
                    self.send_json({'success': False, 'error': 'No PID file found'}, 404)

            elif target == 'main':
                try:
                    result = subprocess.run(
                        ['pgrep', '-f', f'python.*{project_dir.name}/main.py'],
                        capture_output=True, text=True, timeout=3,
                    )
                    if result.stdout.strip():
                        pid = int(result.stdout.strip().split('\n')[0])
                        proc_cwd = Path(f'/proc/{pid}/cwd').resolve()
                        if str(proc_cwd).startswith(str(constants.WORKSPACE_DIR)):
                            os.kill(pid, signal.SIGTERM)
                            self.send_json({'success': True, 'message': f'Main stopped (PID {pid})'})
                        else:
                            self.send_json({'success': False, 'error': 'Process not in workspace'}, 403)
                    else:
                        self.send_json({'success': True, 'message': 'No running process found'})
                except Exception as e:
                    self.send_json({'success': False, 'error': str(e)}, 500)

        except Exception as e:
            self.send_json({'success': False, 'error': str(e)}, 500)

    def handle_workspace_delete(self):
        """Delete a workspace project."""
        try:
            data = self._read_post_json()
            project_name = data.get('project', '')
            confirm = data.get('confirm', '')

            project_dir = self._validate_project(project_name)
            if not project_dir:
                self.send_json({'success': False, 'error': 'Invalid project'}, 400)
                return

            if confirm != project_name:
                self.send_json({'success': False, 'error': 'Confirmation does not match project name'}, 400)
                return

            try:
                result = subprocess.run(
                    ['pgrep', '-f', f'{project_dir.name}/(main\\.py|ralph)'],
                    capture_output=True, text=True, timeout=5,
                )
                if result.stdout.strip():
                    self.send_json({'success': False, 'error': 'Project has running processes. Stop them first.'})
                    return
            except subprocess.TimeoutExpired:
                pass

            shutil.rmtree(str(project_dir))
            self.send_json({'success': True, 'message': f'Project {project_name} deleted'})
        except Exception as e:
            self.send_json({'success': False, 'error': str(e)}, 500)

    def handle_workspace_logs(self):
        """Get last N lines from a project log."""
        try:
            params = parse_qs(urlparse(self.path).query)
            project_name = params.get('project', [''])[0]
            log_type = params.get('type', ['ralph'])[0]
            lines_count = min(int(params.get('lines', ['100'])[0]), 500)

            project_dir = self._validate_project(project_name)
            if not project_dir:
                self.send_json({'error': 'Invalid project'}, 400)
                return

            ralph_dir = project_dir / 'scripts' / 'ralph'

            if log_type == 'ralph':
                log_file = ralph_dir / 'logs' / 'ralph.log'
                if not log_file.exists():
                    log_file = ralph_dir / 'logs' / 'ralph-test.log'
            elif log_type == 'main':
                log_file = ralph_dir / 'logs' / 'main.log' if ralph_dir.exists() else project_dir / 'main.log'
            elif log_type == 'progress':
                log_file = ralph_dir / 'progress.txt'
            elif log_type.startswith('iteration-'):
                log_file = ralph_dir / 'logs' / f'{log_type}.log'
            else:
                self.send_json({'error': 'Invalid log type'}, 400)
                return

            if not log_file.exists():
                self.send_json({'project': project_name, 'type': log_type, 'lines': [], 'total': 0})
                return

            if not str(log_file.resolve()).startswith(str(constants.WORKSPACE_DIR)):
                self.send_json({'error': 'Access denied'}, 403)
                return

            content = log_file.read_text(encoding='utf-8', errors='replace')
            all_lines = content.split('\n')
            total = len(all_lines)
            last_lines = all_lines[-lines_count:] if len(all_lines) > lines_count else all_lines

            self.send_json({
                'project': project_name,
                'type': log_type,
                'lines': last_lines,
                'total': total,
            })
        except Exception as e:
            self.send_json({'error': str(e)}, 500)
