#!/usr/bin/env python3
"""Echo Task Board API — thin HTTP router.

All endpoint logic lives in `dashboard/handlers/*.py`. This file is
responsible only for URL dispatch, CORS, JSON response helpers, and
server bootstrap.
"""
import json
import os
import sys
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import quote as _urlquote, parse_qs, urlparse

# Make dashboard/ importable for the handler submodules (constants,
# habits_helpers, handlers.*). Tests rely on this as well.
_DASH = Path(__file__).parent
if str(_DASH) not in sys.path:
    sys.path.insert(0, str(_DASH))

from constants import (  # noqa: E402  re-exported for tests
    ALLOWED_WORKSPACES,
    BASE_DIR,
    ECHO_CORE_DIR,
    ECHO_LOG_FILE,
    ECHO_SESSIONS_FILE,
    ECO_SERVICES,
    GIT_WORKSPACE,
    GITEA_ORG,
    GITEA_TOKEN,
    GITEA_URL,
    HABITS_FILE,
    KANBAN_DIR,
    NOTES_DIR,
    TOOLS_DIR,
    VENV_PYTHON,
    WORKSPACE_DIR,
)
from handlers.auth import AuthHandlers  # noqa: E402
from handlers.cron import CronHandlers  # noqa: E402
from handlers.eco import EcoHandlers  # noqa: E402
from handlers.files import FilesHandlers  # noqa: E402
from handlers.git import GitHandlers  # noqa: E402
from handlers.habits import HabitsHandlers  # noqa: E402
from handlers.pdf import PDFHandlers  # noqa: E402
from handlers.projects import ProjectsHandlers  # noqa: E402
from handlers.ralph import RalphHandlers  # noqa: E402
from handlers.workspace import WorkspaceHandlers  # noqa: E402
from handlers.youtube import YoutubeHandlers  # noqa: E402

# Shared navigation injected into every served .html via <!--NAV--> marker.
NAV_HTML = '''<header class="header">
    <a href="/echo/index.html" class="logo">
        <i data-lucide="circle-dot"></i>
        Echo
    </a>
    <nav class="nav">
        <a href="/echo/index.html" class="nav-item" data-page="index">
            <i data-lucide="layout-dashboard"></i>
            <span>Dashboard</span>
        </a>
        <a href="/echo/workspace.html" class="nav-item" data-page="workspace">
            <i data-lucide="code"></i>
            <span>Workspace</span>
        </a>
        <a href="/echo/notes.html" class="nav-item" data-page="notes">
            <i data-lucide="file-text"></i>
            <span>KB</span>
        </a>
        <a href="/echo/habits.html" class="nav-item" data-page="habits">
            <i data-lucide="dumbbell"></i>
            <span>Habits</span>
        </a>
        <a href="/echo/files.html" class="nav-item" data-page="files">
            <i data-lucide="folder"></i>
            <span>Files</span>
        </a>
        <button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
            <i data-lucide="sun" id="themeIcon"></i>
        </button>
    </nav>
</header>
<script>
(function(){
    var path = window.location.pathname;
    var m = path.match(/([^\\/]+?)(?:\\.html)?$/);
    var page = m ? m[1] : 'index';
    if (!page || page === 'echo') page = 'index';
    var item = document.querySelector('.nav-item[data-page="' + page + '"]');
    if (item) item.classList.add('active');
})();
</script>'''


class TaskBoardHandler(
    AuthHandlers,
    ProjectsHandlers,
    GitHandlers,
    HabitsHandlers,
    EcoHandlers,
    FilesHandlers,
    PDFHandlers,
    YoutubeHandlers,
    WorkspaceHandlers,
    RalphHandlers,
    CronHandlers,
    SimpleHTTPRequestHandler,
):
    """HTTP request handler — dispatches to handler-mixin methods."""

    # ── shared utilities ────────────────────────────────────────
    def _read_post_json(self):
        """Read a JSON body from the POST request."""
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length).decode('utf-8')
        return json.loads(post_data)

    def send_json(self, data, code=200):
        self.send_response(code)
        self.send_header('Content-Type', 'application/json')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
        self.send_header('Pragma', 'no-cache')
        self.send_header('Expires', '0')
        self.end_headers()
        self.wfile.write(json.dumps(data).encode())

    def do_OPTIONS(self):
        self.send_response(200)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.end_headers()

    # ── dispatch ────────────────────────────────────────────────
    def do_GET(self):
        from datetime import datetime as _dt
        import os
        # Static assets — served directly from dashboard/static/. Handles the
        # case where the URL is hit with the /echo/ prefix intact (e.g. direct
        # localhost curl); when behind the reverse proxy that strips /echo/,
        # the request falls through to SimpleHTTPRequestHandler which serves
        # cwd/static/ naturally (cwd is set to KANBAN_DIR/dashboard).
        if self.path.startswith('/echo/static/'):
            rel = self.path[len('/echo/static/'):].split('?', 1)[0]
            file_path = os.path.join(os.path.dirname(__file__), 'static', rel)
            if os.path.isfile(file_path):
                ext = os.path.splitext(rel)[1].lstrip('.').lower()
                ctype = {
                    'css':   'text/css',
                    'woff2': 'font/woff2',
                    'woff':  'font/woff',
                    'js':    'application/javascript',
                    'svg':   'image/svg+xml',
                    'png':   'image/png',
                }.get(ext, 'application/octet-stream')
                with open(file_path, 'rb') as f:
                    data = f.read()
                self.send_response(200)
                self.send_header('Content-Type', ctype)
                self.send_header('Content-Length', str(len(data)))
                self.send_header('Cache-Control', 'public, max-age=86400')
                self.end_headers()
                self.wfile.write(data)
            else:
                self.send_error(404)
            return
        if self.path == '/api/status':
            self.send_json({'status': 'ok', 'time': _dt.now().isoformat()})
        elif self.path == '/api/git' or self.path.startswith('/api/git?'):
            self.handle_git_status()
        elif self.path == '/api/cron' or self.path.startswith('/api/cron?'):
            self.handle_cron_status()
        elif self.path == '/api/habits':
            self.handle_habits_get()
        elif self.path.startswith('/api/files'):
            self.handle_files_get()
        elif self.path.startswith('/api/diff'):
            self.handle_git_diff()
        elif self.path == '/api/workspace' or self.path.startswith('/api/workspace?'):
            self.handle_workspace_list()
        elif self.path.startswith('/api/workspace/git/diff'):
            self.handle_workspace_git_diff()
        elif self.path.startswith('/api/workspace/logs'):
            self.handle_workspace_logs()
        elif self.path == '/api/eco/status' or self.path.startswith('/api/eco/status?'):
            self.handle_eco_status()
        elif self.path == '/api/eco/sessions' or self.path.startswith('/api/eco/sessions?'):
            self.handle_eco_sessions()
        elif self.path.startswith('/api/eco/sessions/content'):
            self.handle_eco_session_content()
        elif self.path.startswith('/api/eco/logs'):
            self.handle_eco_logs()
        elif self.path == '/api/eco/doctor':
            self.handle_eco_doctor()
        elif self.path == '/api/ralph/status' or self.path.startswith('/api/ralph/status?'):
            self.handle_ralph_status()
        elif self.path == '/api/ralph/usage' or self.path.startswith('/api/ralph/usage?'):
            self.handle_ralph_usage()
        elif self.path == '/api/ralph/stream' or self.path.startswith('/api/ralph/stream?'):
            self.handle_ralph_stream()
        elif self.path.startswith('/api/ralph/'):
            # /api/ralph/<slug>/log or /api/ralph/<slug>/prd
            parts = self.path.split('?', 1)[0].split('/')
            # parts: ['', 'api', 'ralph', '<slug>', '<action>']
            if len(parts) >= 5:
                slug = parts[3]
                action = parts[4]
                if action == 'log':
                    self.handle_ralph_log(slug)
                elif action == 'prd':
                    self.handle_ralph_prd(slug)
                else:
                    self.send_error(404)
            else:
                self.send_error(404)
        elif self.path == '/api/projects' or self.path.startswith('/api/projects?'):
            self.handle_unified_status()
        elif self.path == '/api/projects/signature' or self.path.startswith('/api/projects/signature?'):
            self.handle_unified_signature()
        elif self.path == '/api/projects/stream' or self.path.startswith('/api/projects/stream?'):
            self.handle_projects_stream()
        elif self.path.startswith('/api/projects/'):
            # /api/projects/<slug>/plan/(state|transcript)
            parts = self.path.split('?', 1)[0].split('/')
            # parts: ['', 'api', 'projects', '<slug>', 'plan', '<action>']
            if len(parts) >= 6 and parts[4] == 'plan':
                slug = parts[3]
                action = parts[5]
                if action == 'state':
                    self.handle_plan_state(slug)
                elif action == 'transcript':
                    self.handle_plan_transcript(slug)
                else:
                    self.send_error(404)
            else:
                self.send_error(404)
        elif self.path in ('/', '/echo', '/echo/'):
            self.send_response(302)
            self.send_header('Location', '/echo/index.html')
            self.send_header('Content-Length', '0')
            self.end_headers()
            return
        elif self.path in ('/echo/login', '/login') or \
                self.path.startswith(('/echo/login?', '/login?')):
            # If already logged in, redirect to next (or workspace); otherwise serve login.html.
            if self._check_dashboard_cookie():
                qs = parse_qs(urlparse(self.path).query)
                next_vals = qs.get('next', [])
                nxt = next_vals[0] if next_vals else ''
                # Proxy strips /echo/ before Python, so nxt is e.g. /workspace.html.
                # Re-add the prefix so the browser lands on the right public URL.
                if nxt and nxt.startswith('/') and '://' not in nxt:
                    dest = '/echo' + nxt
                else:
                    dest = '/echo/workspace.html'
                self.send_response(302)
                self.send_header('Location', dest)
                self.send_header('Content-Length', '0')
                self.end_headers()
                return
            login_html = KANBAN_DIR / 'login.html'
            if login_html.is_file():
                body = login_html.read_text('utf-8').replace('<!--NAV-->', NAV_HTML).encode('utf-8')
                self.send_response(200)
                self.send_header('Content-Type', 'text/html; charset=utf-8')
                self.send_header('Content-Length', str(len(body)))
                self.send_header('Cache-Control', 'no-cache')
                self.end_headers()
                self.wfile.write(body)
            else:
                # Lane B2 hasn't shipped yet — return 503 with a hint.
                self.send_error(503, 'login.html not yet available')
        elif self.path == '/ralph.html' or self.path.startswith('/ralph.html?'):
            # Legacy redirect — Ralph dashboard merged into workspace.html (Lane D1).
            self.send_response(302)
            self.send_header('Location', '/echo/workspace.html')
            self.send_header('Content-Length', '0')
            self.end_headers()
            return
        elif self.path.startswith('/api/'):
            self.send_error(404)
        else:
            # Inject shared nav into served HTML pages via <!--NAV--> marker.
            rel = self.path.lstrip('/').split('?')[0]
            if rel.endswith('.html'):
                try:
                    fpath = (KANBAN_DIR / rel).resolve()
                    fpath.relative_to(KANBAN_DIR.resolve())
                except (ValueError, OSError):
                    self.send_error(403)
                    return
                if fpath.is_file():
                    if fpath.name != 'login.html' and not self._check_dashboard_cookie():
                        self.send_response(302)
                        next_param = _urlquote(self.path, safe='/?=&#')
                        self.send_header('Location', f'/echo/login?next={next_param}')
                        self.send_header('Content-Length', '0')
                        self.end_headers()
                        return
                    html = fpath.read_text('utf-8').replace('<!--NAV-->', NAV_HTML)
                    body = html.encode('utf-8')
                    self.send_response(200)
                    self.send_header('Content-Type', 'text/html; charset=utf-8')
                    self.send_header('Content-Length', str(len(body)))
                    self.send_header('Cache-Control', 'no-cache')
                    self.end_headers()
                    self.wfile.write(body)
                    return
            super().do_GET()

    # POSTs that bypass the auth middleware (login itself can't require a cookie).
    UNPROTECTED_POSTS = frozenset({'/api/auth/login'})

    def do_POST(self):
        # ── Auth middleware ────────────────────────────────────────
        # Only protect /api/* POSTs for now — older endpoints predate auth and
        # we want a single, well-defined gate. Static asset POSTs (none today)
        # would also fall through.
        path_only = self.path.split('?', 1)[0]
        if path_only.startswith('/api/') and path_only not in self.UNPROTECTED_POSTS:
            if not self._check_dashboard_cookie():
                body = b'{"error":"Unauthorized"}'
                self.send_response(401)
                self.send_header('Content-Type', 'application/json')
                self.send_header('Content-Length', str(len(body)))
                self.send_header('Cache-Control', 'no-store')
                self.end_headers()
                try:
                    self.wfile.write(body)
                except (BrokenPipeError, ConnectionResetError):
                    pass
                return
            # CSRF: require Origin (or Referer) to be on the allowlist.
            origin = self.headers.get('Origin', '') or ''
            referer = self.headers.get('Referer', '') or ''
            allowed = ['http://127.0.0.1:8088', 'http://localhost:8088']
            dh = os.environ.get('DASHBOARD_HOST', '').strip()
            if dh:
                allowed.append(dh)
            check = origin or referer
            if check and not any(check.startswith(a) for a in allowed):
                body = b'{"error":"CSRF"}'
                self.send_response(403)
                self.send_header('Content-Type', 'application/json')
                self.send_header('Content-Length', str(len(body)))
                self.send_header('Cache-Control', 'no-store')
                self.end_headers()
                try:
                    self.wfile.write(body)
                except (BrokenPipeError, ConnectionResetError):
                    pass
                return

        if self.path == '/api/youtube':
            self.handle_youtube()
        elif self.path == '/api/files':
            self.handle_files_post()
        elif self.path == '/api/refresh-index':
            self.handle_refresh_index()
        elif self.path == '/api/pdf':
            self.handle_pdf_post()
        elif self.path == '/api/habits':
            self.handle_habits_post()
        elif self.path.startswith('/api/habits/') and self.path.endswith('/check'):
            self.handle_habits_check()
        elif self.path.startswith('/api/habits/') and self.path.endswith('/skip'):
            self.handle_habits_skip()
        elif self.path == '/api/workspace/run':
            self.handle_workspace_run()
        elif self.path == '/api/workspace/stop':
            self.handle_workspace_stop()
        elif self.path == '/api/workspace/git/commit':
            self.handle_workspace_git_commit()
        elif self.path == '/api/workspace/git/push':
            self.handle_workspace_git_push()
        elif self.path == '/api/workspace/delete':
            self.handle_workspace_delete()
        elif self.path == '/api/eco/restart':
            self.handle_eco_restart()
        elif self.path == '/api/eco/stop':
            self.handle_eco_stop()
        elif self.path == '/api/eco/sessions/clear':
            self.handle_eco_sessions_clear()
        elif self.path == '/api/eco/git-commit':
            self.handle_eco_git_commit()
        elif self.path == '/api/eco/restart-taskboard':
            self.handle_eco_restart_taskboard()
        elif self.path.startswith('/api/ralph/'):
            # /api/ralph/<slug>/{stop,rollback}
            parts = self.path.split('?', 1)[0].split('/')
            if len(parts) >= 5:
                slug = parts[3]
                action = parts[4]
                if action == 'stop':
                    self.handle_ralph_stop(slug)
                elif action == 'rollback':
                    self.handle_ralph_rollback(slug)
                else:
                    self.send_error(404)
            else:
                self.send_error(404)
        elif self.path == '/api/auth/login':
            self.handle_login()
        elif self.path == '/api/auth/logout':
            self.handle_logout()
        elif self.path == '/api/projects/propose':
            self.handle_propose()
        elif self.path == '/api/projects/approve':
            self.handle_approve()
        elif self.path == '/api/projects/unapprove':
            self.handle_unapprove()
        elif self.path == '/api/projects/cancel':
            self.handle_cancel()
        elif self.path.startswith('/api/projects/'):
            # /api/projects/<slug>/plan/(start|respond|finalize|cancel|advance)
            parts = self.path.split('?', 1)[0].split('/')
            # parts: ['', 'api', 'projects', '<slug>', 'plan', '<action>']
            if len(parts) >= 6 and parts[4] == 'plan':
                slug = parts[3]
                action = parts[5]
                if action == 'start':
                    self.handle_plan_start(slug)
                elif action == 'respond':
                    self.handle_plan_respond(slug)
                elif action == 'finalize':
                    self.handle_plan_finalize(slug)
                elif action == 'cancel':
                    self.handle_plan_cancel_planning(slug)
                elif action == 'advance':
                    self.handle_plan_advance(slug)
                else:
                    self.send_error(404)
            else:
                self.send_error(404)
        else:
            self.send_error(404)

    def do_PUT(self):
        if self.path.startswith('/api/habits/'):
            self.handle_habits_put()
        else:
            self.send_error(404)

    def do_DELETE(self):
        if self.path.startswith('/api/habits/') and '/check' in self.path:
            self.handle_habits_uncheck()
        elif self.path.startswith('/api/habits/'):
            self.handle_habits_delete()
        else:
            self.send_error(404)


if __name__ == '__main__':
    import os
    port = 8088
    os.chdir(KANBAN_DIR)

    print(f"Starting Echo Task Board API on port {port}")
    # ThreadingHTTPServer permite SSE long-lived (/api/ralph/stream) fără să
    # blocheze celelalte request-uri.
    httpd = ThreadingHTTPServer(('0.0.0.0', port), TaskBoardHandler)
    httpd.daemon_threads = True
    httpd.serve_forever()
