"""AYN Antivirus Dashboard — Web Server with Password Auth.

Lightweight aiohttp server that serves the dashboard SPA and REST API.
Non-localhost access requires username/password authentication via a
session cookie obtained through ``POST /login``.
"""

from __future__ import annotations

import logging
import secrets
import time
from typing import Dict, Optional
from urllib.parse import urlparse

from aiohttp import web

from ayn_antivirus.config import Config
from ayn_antivirus.constants import QUARANTINE_ENCRYPTION_KEY_FILE
from ayn_antivirus.dashboard.api import setup_routes
from ayn_antivirus.dashboard.collector import MetricsCollector
from ayn_antivirus.dashboard.store import DashboardStore
from ayn_antivirus.dashboard.templates import get_dashboard_html

logger = logging.getLogger("ayn_antivirus.dashboard.server")


# ------------------------------------------------------------------
# JSON error handler — prevent aiohttp returning HTML on /api/* routes
# ------------------------------------------------------------------

@web.middleware
async def json_error_middleware(
    request: web.Request,
    handler,
) -> web.StreamResponse:
    """Catch unhandled exceptions and return JSON for API routes.

    Without this, aiohttp's default error handler returns HTML error
    pages, which break frontend ``fetch().json()`` calls.
    """
    try:
        return await handler(request)
    except web.HTTPException as exc:
        if request.path.startswith("/api/"):
            return web.json_response(
                {"error": exc.reason or "Request failed"},
                status=exc.status,
            )
        raise
    except Exception as exc:
        logger.exception("Unhandled error on %s %s", request.method, request.path)
        if request.path.startswith("/api/"):
            return web.json_response(
                {"error": f"Internal server error: {exc}"},
                status=500,
            )
        return web.Response(
            text="<h1>500 Internal Server Error</h1>",
            status=500,
            content_type="text/html",
        )

# ------------------------------------------------------------------
# Rate limiting state
# ------------------------------------------------------------------

_action_timestamps: Dict[str, float] = {}
_RATE_LIMIT_SECONDS = 10


# ------------------------------------------------------------------
# Authentication middleware
# ------------------------------------------------------------------


@web.middleware
async def auth_middleware(
    request: web.Request,
    handler,
) -> web.StreamResponse:
    """Authenticate all requests.

    * ``/login`` and ``/favicon.ico`` are always allowed.
    * All other routes require a valid session cookie.
    * Unauthenticated HTML routes serve the login page.
    * Unauthenticated ``/api/*`` returns 401.
    * POST ``/api/actions/*`` enforces CSRF and rate limiting.
    """
    # Login route is always open.
    if request.path in ("/login", "/favicon.ico"):
        return await handler(request)

    # All requests require auth (no localhost bypass — behind reverse proxy).
    # Check session cookie.
    session_token = request.app.get("_session_token", "")
    cookie = request.cookies.get("ayn_session", "")
    authenticated = (
        cookie
        and session_token
        and secrets.compare_digest(cookie, session_token)
    )

    if not authenticated:
        if request.path.startswith("/api/"):
            return web.json_response(
                {"error": "Unauthorized. Please login."}, status=401,
            )
        # Serve login page for HTML routes.
        return web.Response(
            text=request.app["_login_html"], content_type="text/html",
        )

    # CSRF + rate-limiting for POST action endpoints.
    if request.method == "POST" and request.path.startswith("/api/actions/"):
        origin = request.headers.get("Origin", "")
        if origin:
            parsed = urlparse(origin)
            origin_host = parsed.hostname or ""
            host = request.headers.get("Host", "")
            expected = host.split(":")[0] if host else ""
            allowed = {expected, "localhost", "127.0.0.1", "::1"}
            allowed.discard("")
            if origin_host not in allowed:
                return web.json_response(
                    {"error": "CSRF: Origin mismatch"}, status=403,
                )

        now = time.time()
        last = _action_timestamps.get(request.path, 0)
        if now - last < _RATE_LIMIT_SECONDS:
            return web.json_response(
                {"error": "Rate limited. Try again in a few seconds."},
                status=429,
            )
        _action_timestamps[request.path] = now

    return await handler(request)


# ------------------------------------------------------------------
# Dashboard server
# ------------------------------------------------------------------


class DashboardServer:
    """AYN Antivirus dashboard with username/password authentication."""

    def __init__(self, config: Optional[Config] = None) -> None:
        self.config = config or Config()
        self.store = DashboardStore(self.config.dashboard_db_path)
        self.collector = MetricsCollector(self.store)
        self.app = web.Application(middlewares=[json_error_middleware, auth_middleware])
        self._session_token: str = secrets.token_urlsafe(32)
        self._runner: Optional[web.AppRunner] = None
        self._site: Optional[web.TCPSite] = None
        self._setup()

    # ------------------------------------------------------------------
    # Setup
    # ------------------------------------------------------------------

    def _setup(self) -> None:
        """Configure the aiohttp application."""
        self.app["_session_token"] = self._session_token
        self.app["_login_html"] = self._build_login_page()

        self.app["store"] = self.store
        self.app["collector"] = self.collector
        self.app["config"] = self.config

        # Quarantine vault (best-effort).
        try:
            from ayn_antivirus.quarantine.vault import QuarantineVault

            self.app["vault"] = QuarantineVault(
                quarantine_dir=self.config.quarantine_path,
                key_file_path=QUARANTINE_ENCRYPTION_KEY_FILE,
            )
        except Exception as exc:
            logger.warning("Quarantine vault not available: %s", exc)

        # API routes (``/api/*``).
        setup_routes(self.app)

        # HTML routes.
        self.app.router.add_get("/", self._serve_dashboard)
        self.app.router.add_get("/dashboard", self._serve_dashboard)
        self.app.router.add_get("/login", self._serve_login)
        self.app.router.add_post("/login", self._handle_login)

        # Lifecycle hooks.
        self.app.on_startup.append(self._on_startup)
        self.app.on_shutdown.append(self._on_shutdown)

    # ------------------------------------------------------------------
    # Request handlers
    # ------------------------------------------------------------------

    async def _serve_login(self, request: web.Request) -> web.Response:
        """``GET /login`` — render the login page."""
        return web.Response(
            text=self.app["_login_html"], content_type="text/html",
        )

    async def _serve_dashboard(self, request: web.Request) -> web.Response:
        """``GET /`` or ``GET /dashboard`` — render the SPA.

        The middleware already enforces auth for non-localhost, so if we
        reach here the client is authenticated (or local).
        """
        html = get_dashboard_html()
        return web.Response(text=html, content_type="text/html")

    async def _handle_login(self, request: web.Request) -> web.Response:
        """``POST /login`` — validate username/password, set session cookie."""
        try:
            body = await request.json()
            username = body.get("username", "").strip()
            password = body.get("password", "").strip()
        except Exception:
            return web.json_response({"error": "Invalid request"}, status=400)

        if not username or not password:
            return web.json_response(
                {"error": "Username and password required"}, status=400,
            )

        valid_user = secrets.compare_digest(
            username, self.config.dashboard_username,
        )
        valid_pass = secrets.compare_digest(
            password, self.config.dashboard_password,
        )

        if not (valid_user and valid_pass):
            self.store.log_activity(
                f"Failed login attempt from {request.remote}: user={username}",
                "WARNING",
                "auth",
            )
            return web.json_response(
                {"error": "Invalid username or password"}, status=401,
            )

        self.store.log_activity(
            f"Successful login from {request.remote}: user={username}",
            "INFO",
            "auth",
        )
        response = web.json_response(
            {"status": "ok", "message": "Welcome to AYN Antivirus"},
        )
        response.set_cookie(
            "ayn_session",
            self._session_token,
            httponly=True,
            max_age=86400,
            samesite="Strict",
        )
        return response

    # ------------------------------------------------------------------
    # Login page
    # ------------------------------------------------------------------

    @staticmethod
    def _build_login_page() -> str:
        """Return a polished HTML login form with username + password fields."""
        return '''<!DOCTYPE html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>AYN Antivirus \u2014 Login</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0e17;color:#e2e8f0;font-family:'Segoe UI',system-ui,-apple-system,sans-serif;
  display:flex;justify-content:center;align-items:center;min-height:100vh;
  background-image:radial-gradient(circle at 50% 50%,#111827 0%,#0a0e17 70%)}
.login-box{background:#111827;padding:2.5rem;border-radius:16px;border:1px solid #2a3444;
  width:420px;max-width:90vw;box-shadow:0 25px 80px rgba(0,0,0,0.6)}
.logo{text-align:center;margin-bottom:2rem}
.logo .shield{font-size:3.5rem;display:block;margin-bottom:0.5rem}
.logo h1{font-size:1.8rem;background:linear-gradient(135deg,#3b82f6,#06b6d4);
  -webkit-background-clip:text;-webkit-text-fill-color:transparent;font-weight:800}
.logo .subtitle{color:#6b7280;font-size:0.8rem;margin-top:0.3rem;letter-spacing:2px;text-transform:uppercase}
.field{margin-bottom:1.2rem}
.field label{display:block;font-size:0.75rem;color:#9ca3af;margin-bottom:0.4rem;
  text-transform:uppercase;letter-spacing:1px;font-weight:600}
.field input{width:100%;padding:12px 16px;background:#0d1117;border:1px solid #2a3444;
  color:#e2e8f0;border-radius:10px;font-size:15px;transition:all 0.2s;outline:none}
.field input:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,0.15)}
.field input::placeholder{color:#4b5563}
.btn{width:100%;padding:13px;background:linear-gradient(135deg,#3b82f6,#2563eb);color:white;
  border:none;border-radius:10px;cursor:pointer;font-size:15px;font-weight:700;
  transition:all 0.2s;margin-top:0.8rem;letter-spacing:0.5px}
.btn:hover{transform:translateY(-2px);box-shadow:0 8px 25px rgba(59,130,246,0.4)}
.btn:active{transform:translateY(0)}
.btn:disabled{opacity:0.5;cursor:not-allowed;transform:none}
.error{color:#fca5a5;font-size:0.85rem;text-align:center;margin-top:1rem;padding:10px 16px;
  background:rgba(239,68,68,0.1);border-radius:8px;display:none;border:1px solid rgba(239,68,68,0.2)}
.footer{text-align:center;margin-top:2rem;padding-top:1.5rem;border-top:1px solid #1e293b}
.footer p{color:#4b5563;font-size:0.7rem;line-height:1.8}
.spinner{display:inline-block;width:16px;height:16px;border:2px solid #fff;
  border-top-color:transparent;border-radius:50%;animation:spin 0.6s linear infinite;
  vertical-align:middle;margin-right:8px}
@keyframes spin{to{transform:rotate(360deg)}}
</style></head>
<body>
<div class="login-box">
  <div class="logo">
    <span class="shield">\U0001f6e1\ufe0f</span>
    <h1>AYN ANTIVIRUS</h1>
    <div class="subtitle">Security Operations Dashboard</div>
  </div>
  <form id="loginForm" onsubmit="return doLogin()">
    <div class="field">
      <label>Username</label>
      <input type="text" id="username" placeholder="Enter username" autocomplete="username" autofocus required>
    </div>
    <div class="field">
      <label>Password</label>
      <input type="password" id="password" placeholder="Enter password" autocomplete="current-password" required>
    </div>
    <button type="submit" class="btn" id="loginBtn">\U0001f510 Sign In</button>
  </form>
  <div class="error" id="errMsg">Invalid credentials</div>
  <div class="footer">
    <p>AYN Antivirus v1.0.0 \u2014 Server Protection Suite<br>
    Secure Access Portal</p>
  </div>
</div>
<script>
async function doLogin(){
  var btn=document.getElementById('loginBtn');
  var err=document.getElementById('errMsg');
  var user=document.getElementById('username').value.trim();
  var pass=document.getElementById('password').value;
  if(!user||!pass)return false;
  err.style.display='none';
  btn.disabled=true;
  btn.innerHTML='<span class="spinner"></span>Signing in...';
  try{
    var r=await fetch('/login',{method:'POST',
      headers:{'Content-Type':'application/json'},
      body:JSON.stringify({username:user,password:pass})});
    if(r.ok){window.location.href='/dashboard';}
    else{var d=await r.json();err.textContent=d.error||'Invalid credentials';err.style.display='block';}
  }catch(e){err.textContent='Connection failed. Check server.';err.style.display='block';}
  btn.disabled=false;btn.innerHTML='\\U0001f510 Sign In';
  return false;
}
document.querySelectorAll('input').forEach(function(i){i.addEventListener('input',function(){
  document.getElementById('errMsg').style.display='none';
});});
</script>
</body></html>'''

    # ------------------------------------------------------------------
    # Lifecycle hooks
    # ------------------------------------------------------------------

    async def _on_startup(self, app: web.Application) -> None:
        await self.collector.start()
        self.store.log_activity("Dashboard server started", "INFO", "server")
        logger.info(
            "Dashboard on http://%s:%d",
            self.config.dashboard_host,
            self.config.dashboard_port,
        )

    async def _on_shutdown(self, app: web.Application) -> None:
        await self.collector.stop()
        self.store.log_activity("Dashboard server stopped", "INFO", "server")
        self.store.close()

    # ------------------------------------------------------------------
    # Blocking run
    # ------------------------------------------------------------------

    def run(self) -> None:
        """Run the dashboard server (blocking)."""
        host = self.config.dashboard_host
        port = self.config.dashboard_port
        print(f"\n  \U0001f6e1\ufe0f  AYN Antivirus Dashboard")
        print(f"  \U0001f310 http://{host}:{port}")
        print(f"  \U0001f464 Username: {self.config.dashboard_username}")
        print(f"  \U0001f511 Password: {self.config.dashboard_password}")
        print(f"  Press Ctrl+C to stop\n")
        web.run_app(self.app, host=host, port=port, print=None)

    # ------------------------------------------------------------------
    # Async start / stop (non-blocking)
    # ------------------------------------------------------------------

    async def start_async(self) -> None:
        """Start the server without blocking."""
        self._runner = web.AppRunner(self.app)
        await self._runner.setup()
        self._site = web.TCPSite(
            self._runner,
            self.config.dashboard_host,
            self.config.dashboard_port,
        )
        await self._site.start()
        self.store.log_activity(
            "Dashboard server started (async)", "INFO", "server",
        )

    async def stop_async(self) -> None:
        """Stop a server previously started with :meth:`start_async`."""
        if self._site:
            await self._site.stop()
        if self._runner:
            await self._runner.cleanup()
        await self.collector.stop()
        self.store.close()


# ------------------------------------------------------------------
# Convenience entry point
# ------------------------------------------------------------------


def run_dashboard(config: Optional[Config] = None) -> None:
    """Create a :class:`DashboardServer` and run it (blocking)."""
    DashboardServer(config).run()
