#!/usr/bin/env python3
from __future__ import annotations

import json
import os
import re
import subprocess
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any

HOST = os.environ.get("SHIRE_MOBILE_GATEWAY_HOST", "127.0.0.1")
PORT = int(os.environ.get("SHIRE_MOBILE_GATEWAY_PORT", "8795"))
TOKEN = os.environ.get("SHIRE_MOBILE_GATEWAY_TOKEN", "")
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434").rstrip("/")
FAST_MODEL = os.environ.get("SHIRE_FAST_MODEL", "shire-mini-fast:qwen3.5-4b")
DEEP_MODEL = os.environ.get("SHIRE_DEEP_MODEL", "shire-mini-deep:qwen3.5-9b")

SENTINEL_DATA = Path("/home/shire3d/ARMOR/data/sentinel")
ACTION_ROOT = Path("/home/ray/.local/share/shire-mobile-gateway")
ACTION_LOG = ACTION_ROOT / "sentinel-actions.jsonl"

MAX_BODY = 256 * 1024
MAX_MESSAGE = 8000
MAX_HISTORY = 30
ALLOWED_ACTIONS = {
    "acknowledge",
    "request_investigation",
    "mark_false_positive",
    "defer",
}

SYSTEM_PROMPT = """You are SHiRE, Ray's private local AI command companion running on SHiRE Mini.
Address Ray naturally as Chief when appropriate. Be warm, direct and practical.
You may explain, plan, analyse and help with defensive cyber-security on Ray's own
devices and networks. Never claim that a destructive, external, purchasing,
publishing, printing, firewall, router or Forge action happened unless an audited
approved tool reports that it happened. Keep Forge locked unless separately
approved. Do not provide offensive intrusion guidance. When uncertain, say so.
This mobile channel is private through Tailscale, but still avoid exposing secrets."""

SCAM_PROMPT = """You are SHiRE Scam Check, a defensive analyst. Analyse only the
provided message, URL, phone number, QR payload or description. Explain concrete
warning signs, what is unknown, the safest next steps, and a risk level from LOW,
MEDIUM, HIGH or CRITICAL. Do not claim to identify a real person or organisation
without evidence. Do not open links or perform external actions. Mention that this
is pattern analysis, not a live reputation lookup, unless live evidence is supplied."""


def now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def run_command(command: list[str], timeout: int = 5) -> tuple[int, str]:
    try:
        completed = subprocess.run(
            command,
            text=True,
            capture_output=True,
            timeout=timeout,
            check=False,
        )
        output = (completed.stdout or completed.stderr or "").strip()
        return completed.returncode, output
    except Exception as exc:
        return 1, str(exc)


def service_state(name: str) -> str:
    code, output = run_command(["systemctl", "is-active", name], timeout=4)
    return output if code == 0 else (output or "unknown")


def json_from_file(path: Path) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8", errors="replace"))
    except Exception:
        return None


def flatten_alerts(value: Any, source: str, found: list[dict[str, Any]]) -> None:
    if len(found) >= 25:
        return

    if isinstance(value, list):
        for item in value[-25:]:
            flatten_alerts(item, source, found)
        return

    if not isinstance(value, dict):
        return

    keys = {str(key).lower() for key in value}
    looks_like_alert = bool(
        keys
        & {
            "alert",
            "signature",
            "severity",
            "event_type",
            "threat",
            "category",
            "message",
            "title",
        }
    )

    if looks_like_alert:
        alert = value.get("alert")
        if isinstance(alert, dict):
            title = (
                alert.get("signature")
                or alert.get("category")
                or value.get("message")
                or "Sentinel alert"
            )
            severity = alert.get("severity") or value.get("severity") or "unknown"
            category = alert.get("category") or value.get("event_type") or "network"
        else:
            title = (
                value.get("signature")
                or value.get("title")
                or value.get("message")
                or value.get("event_type")
                or "Sentinel event"
            )
            severity = value.get("severity") or value.get("priority") or "unknown"
            category = value.get("category") or value.get("event_type") or "security"

        timestamp = (
            value.get("timestamp")
            or value.get("updated_at")
            or value.get("created_at")
            or ""
        )
        identifier = str(
            value.get("id")
            or value.get("event_id")
            or f"{source}:{len(found) + 1}"
        )

        found.append(
            {
                "id": identifier[:160],
                "title": str(title)[:500],
                "severity": str(severity)[:80],
                "category": str(category)[:160],
                "timestamp": str(timestamp)[:160],
                "source": source,
            }
        )

    for child in value.values():
        if isinstance(child, (dict, list)):
            flatten_alerts(child, source, found)


def recent_sentinel_alerts() -> list[dict[str, Any]]:
    found: list[dict[str, Any]] = []
    if not SENTINEL_DATA.exists():
        return found

    paths = sorted(
        [
            path
            for path in SENTINEL_DATA.rglob("*")
            if path.is_file() and path.suffix.lower() in {".json", ".jsonl"}
        ],
        key=lambda path: path.stat().st_mtime,
        reverse=True,
    )[:30]

    for path in paths:
        if len(found) >= 25:
            break
        try:
            if path.suffix.lower() == ".jsonl":
                lines = path.read_text(
                    encoding="utf-8",
                    errors="replace",
                ).splitlines()[-40:]
                for line in lines:
                    try:
                        flatten_alerts(json.loads(line), path.name, found)
                    except Exception:
                        continue
            else:
                flatten_alerts(json_from_file(path), path.name, found)
        except Exception:
            continue

    return found[-20:][::-1]


def listener_summary() -> list[str]:
    code, output = run_command(["ss", "-lntH"], timeout=5)
    if code != 0:
        return []

    listeners: list[str] = []
    for line in output.splitlines():
        fields = line.split()
        if len(fields) < 4:
            continue
        address = fields[3]
        if address.startswith("127.") or address.startswith("[::1]"):
            continue
        listeners.append(address)
    return sorted(set(listeners))[:30]


def sentinel_status() -> dict[str, Any]:
    bridge_path = SENTINEL_DATA / "alert_bridge_status.json"
    bridge = json_from_file(bridge_path)

    return {
        "ok": True,
        "generated_at": now_iso(),
        "boundary": "observe, report and approval-gated response only",
        "services": {
            "sentinel_alert": service_state("shire-sentinel-alert.service"),
            "sentinel_suricata": service_state("shire-sentinel-suricata.service"),
            "tailscale": service_state("tailscaled.service"),
            "brain": service_state("shire-brain.service"),
            "approval_hub": service_state("shire-approval-hub.service"),
        },
        "alert_bridge": bridge if isinstance(bridge, dict) else {},
        "recent_alerts": recent_sentinel_alerts(),
        "non_loopback_listeners": listener_summary(),
        "supported_actions": sorted(ALLOWED_ACTIONS),
        "blocked_actions": [
            "automatic isolation",
            "automatic router blocking",
            "offensive scanning",
            "credential attacks",
        ],
    }


def ollama_chat(messages: list[dict[str, str]], deep: bool = False) -> str:
    model = DEEP_MODEL if deep else FAST_MODEL
    payload = {
        "model": model,
        "messages": messages,
        "stream": False,
        "options": {
            "temperature": 0.45,
            "num_ctx": 4096,
        },
    }
    request = urllib.request.Request(
        OLLAMA_URL + "/api/chat",
        data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    try:
        with urllib.request.urlopen(request, timeout=180) as response:
            body = json.loads(response.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"Local model HTTP {exc.code}: {detail[:500]}") from exc
    except Exception as exc:
        raise RuntimeError(f"Local SHiRE model unavailable: {exc}") from exc

    answer = str((body.get("message") or {}).get("content") or "").strip()
    if not answer:
        raise RuntimeError("Local SHiRE model returned an empty answer.")
    return answer


def chat_response(payload: dict[str, Any]) -> dict[str, Any]:
    message = str(payload.get("message") or "").strip()
    if not message:
        raise ValueError("message is required")
    if len(message) > MAX_MESSAGE:
        raise ValueError("message exceeds the 8000 character mobile limit")

    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    history = payload.get("history")
    if isinstance(history, list):
        for item in history[-MAX_HISTORY:]:
            if not isinstance(item, dict):
                continue
            role = str(item.get("role") or "").lower()
            content = str(item.get("content") or "").strip()
            if role not in {"user", "assistant"} or not content:
                continue
            messages.append({"role": role, "content": content[:MAX_MESSAGE]})

    messages.append({"role": "user", "content": message})
    answer = ollama_chat(messages, deep=bool(payload.get("deep")))
    return {
        "ok": True,
        "answer": answer,
        "model_lane": "deep" if payload.get("deep") else "fast",
        "generated_at": now_iso(),
    }


def scam_response(payload: dict[str, Any]) -> dict[str, Any]:
    content = str(payload.get("content") or "").strip()
    if not content:
        raise ValueError("content is required")
    if len(content) > MAX_MESSAGE:
        raise ValueError("content exceeds the 8000 character limit")

    answer = ollama_chat(
        [
            {"role": "system", "content": SCAM_PROMPT},
            {"role": "user", "content": content},
        ],
        deep=False,
    )
    return {
        "ok": True,
        "analysis": answer,
        "live_reputation_lookup": False,
        "generated_at": now_iso(),
    }


def record_action(payload: dict[str, Any]) -> dict[str, Any]:
    action = str(payload.get("action") or "").strip()
    if action not in ALLOWED_ACTIONS:
        raise ValueError("unsupported or unsafe Sentinel action")

    record = {
        "schema": "shire.mobile.sentinel.action.v1",
        "timestamp": now_iso(),
        "action": action,
        "alert_id": str(payload.get("alert_id") or "")[:200],
        "note": str(payload.get("note") or "")[:2000],
        "effect": "audit only; no firewall, router or service state changed",
    }

    ACTION_ROOT.mkdir(parents=True, exist_ok=True)
    with ACTION_LOG.open("a", encoding="utf-8") as handle:
        handle.write(json.dumps(record, sort_keys=True) + "\n")

    return {"ok": True, "record": record}


class Handler(BaseHTTPRequestHandler):
    server_version = "SHiREMobileGateway/0.7.0"

    def log_message(self, fmt: str, *args: Any) -> None:
        print(
            json.dumps(
                {
                    "timestamp": now_iso(),
                    "client": self.client_address[0],
                    "message": fmt % args,
                }
            ),
            flush=True,
        )

    def _authorised(self) -> bool:
        supplied = self.headers.get("Authorization", "")
        return bool(TOKEN) and supplied == f"Bearer {TOKEN}"

    def _send(self, status: int, payload: dict[str, Any]) -> None:
        body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _read_json(self) -> dict[str, Any]:
        length = int(self.headers.get("Content-Length", "0") or "0")
        if length <= 0 or length > MAX_BODY:
            raise ValueError("invalid request body length")
        value = json.loads(self.rfile.read(length).decode("utf-8"))
        if not isinstance(value, dict):
            raise ValueError("JSON object required")
        return value

    def do_GET(self) -> None:
        if not self._authorised():
            self._send(401, {"ok": False, "error": "unauthorised"})
            return

        if self.path == "/v1/health":
            self._send(
                200,
                {
                    "ok": True,
                    "service": "shire-mobile-gateway",
                    "version": "0.7.0",
                    "private_binding": HOST,
                    "sentinel_mode": "read-only plus audited safe decisions",
                    "voice_model_ready": False,
                    "timestamp": now_iso(),
                },
            )
            return

        if self.path == "/v1/sentinel/status":
            self._send(200, sentinel_status())
            return

        if self.path == "/v1/voice/status":
            self._send(
                200,
                {
                    "ok": True,
                    "enrolment_capture_supported": True,
                    "ray_voice_model_ready": False,
                    "speech_to_text": "Pixel on-device recogniser",
                    "speech_output": "Android local TTS when enabled",
                    "audio_retention": "app-private enrolment samples only after consent",
                },
            )
            return

        self._send(404, {"ok": False, "error": "not found"})

    def do_POST(self) -> None:
        if not self._authorised():
            self._send(401, {"ok": False, "error": "unauthorised"})
            return

        try:
            payload = self._read_json()

            if self.path == "/v1/chat":
                self._send(200, chat_response(payload))
                return

            if self.path == "/v1/scam-check":
                self._send(200, scam_response(payload))
                return

            if self.path == "/v1/sentinel/action":
                self._send(200, record_action(payload))
                return

            self._send(404, {"ok": False, "error": "not found"})
        except ValueError as exc:
            self._send(400, {"ok": False, "error": str(exc)})
        except Exception as exc:
            self._send(503, {"ok": False, "error": str(exc)})


def main() -> None:
    if not TOKEN:
        raise SystemExit("SHIRE_MOBILE_GATEWAY_TOKEN is required")
    ACTION_ROOT.mkdir(parents=True, exist_ok=True)
    server = ThreadingHTTPServer((HOST, PORT), Handler)
    print(
        json.dumps(
            {
                "ok": True,
                "service": "shire-mobile-gateway",
                "host": HOST,
                "port": PORT,
                "started_at": now_iso(),
            }
        ),
        flush=True,
    )
    server.serve_forever()


if __name__ == "__main__":
    main()
