"""SSH-команды на хостах девайсов.

Учётные данные лежат в отдельной БД `ssh_hosts.db`:
имя девайса, IP, пароль, логин (по умолчанию `root`), порт сервера (по умолчанию `22`).
"""

from __future__ import annotations

import re
import sqlite3
import sys
import time
import uuid
from pathlib import Path
from typing import Optional

import paramiko

DB_PATH = str(Path(__file__).resolve().parent / "ssh_hosts.db")
DEFAULT_USER = "root"
DEFAULT_PORT = 22
CONNECT_TIMEOUT = 15


def get_conn():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()
    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS ssh_hosts (
            device TEXT PRIMARY KEY,
            ip TEXT NOT NULL,
            password TEXT,
            username TEXT DEFAULT 'root',
            port INTEGER DEFAULT 22
        )
        """
    )
    cur.execute("PRAGMA table_info(ssh_hosts)")
    cols = {row[1] for row in cur.fetchall()}
    if "username" not in cols:
        cur.execute("ALTER TABLE ssh_hosts ADD COLUMN username TEXT DEFAULT 'root'")
    if "port" not in cols:
        cur.execute("ALTER TABLE ssh_hosts ADD COLUMN port INTEGER DEFAULT 22")
    conn.commit()
    return conn


def upsert_host(
    device: str,
    ip: str,
    password: Optional[str] = None,
    username: Optional[str] = None,
    port: Optional[int] = None,
) -> None:
    device = (device or "").strip()
    ip = (ip or "").strip()
    if not device:
        raise ValueError("Имя девайса обязательно")
    if not ip:
        raise ValueError("IP обязателен")
    existing = get_host(device)
    if password in (None, ""):
        if not existing:
            raise ValueError("Пароль SSH обязателен")
        password = existing.get("password") or ""
    if not username:
        username = (existing or {}).get("username") or DEFAULT_USER
    if port in (None, ""):
        port = (existing or {}).get("port") or DEFAULT_PORT
    try:
        port = int(port)
    except (TypeError, ValueError) as exc:
        raise ValueError("Порт сервера должен быть числом") from exc
    conn = get_conn()
    cur = conn.cursor()
    cur.execute(
        """
        INSERT INTO ssh_hosts (device, ip, password, username, port)
        VALUES (?, ?, ?, ?, ?)
        ON CONFLICT(device) DO UPDATE SET
            ip = excluded.ip,
            password = excluded.password,
            username = excluded.username,
            port = excluded.port
        """,
        (device, ip, password, username or DEFAULT_USER, port),
    )
    conn.commit()
    conn.close()


def get_host(device: str) -> Optional[dict]:
    conn = get_conn()
    cur = conn.cursor()
    cur.execute("SELECT * FROM ssh_hosts WHERE device = ?", (device,))
    row = cur.fetchone()
    conn.close()
    if not row:
        return None
    return dict(row)


def list_hosts() -> list[dict]:
    conn = get_conn()
    cur = conn.cursor()
    cur.execute("SELECT * FROM ssh_hosts")
    rows = cur.fetchall()
    conn.close()
    return [dict(r) for r in rows]


def delete_host(device: str) -> bool:
    conn = get_conn()
    cur = conn.cursor()
    cur.execute("DELETE FROM ssh_hosts WHERE device = ?", (device,))
    conn.commit()
    changed = cur.rowcount
    conn.close()
    return changed > 0


def rename_host(old: str, new: str) -> bool:
    old = (old or "").strip()
    new = (new or "").strip()
    if not old or not new or old == new:
        return False
    conn = get_conn()
    cur = conn.cursor()
    try:
        cur.execute("UPDATE ssh_hosts SET device = ? WHERE device = ?", (new, old))
        conn.commit()
        changed = cur.rowcount
    except sqlite3.IntegrityError as exc:
        conn.close()
        raise ValueError(f"SSH-запись '{new}' уже есть") from exc
    conn.close()
    return changed > 0


def _normalize_commands(commands) -> list[str]:
    if commands is None:
        return []
    if isinstance(commands, str):
        items = commands.splitlines()
    else:
        try:
            items = list(commands)
        except TypeError:
            items = [commands]
    return [str(item).strip() for item in items if str(item).strip()]


def _connect_host(device_name: str) -> tuple[paramiko.SSHClient, str, int]:
    device_name = (device_name or "").strip()
    if not device_name:
        raise ValueError("Имя девайса обязательно")

    host = get_host(device_name)
    if not host:
        raise RuntimeError(f"SSH-хост для девайса '{device_name}' не найден в ssh_hosts.db")

    ip = (host.get("ip") or "").strip()
    if not ip:
        raise RuntimeError(f"У девайса '{device_name}' не указан IP в ssh_hosts.db")

    username = (host.get("username") or DEFAULT_USER).strip() or DEFAULT_USER
    try:
        port = int(host.get("port") or DEFAULT_PORT)
    except (TypeError, ValueError):
        port = DEFAULT_PORT
    password = host.get("password") or ""

    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect(
        hostname=ip,
        port=port,
        username=username,
        password=password,
        timeout=CONNECT_TIMEOUT,
        banner_timeout=CONNECT_TIMEOUT,
        auth_timeout=CONNECT_TIMEOUT,
        allow_agent=False,
        look_for_keys=False,
    )
    return client, ip, port


def _parse_session_output(text: str, token: str, commands: list[str], shell_exit: int) -> list[dict]:
    pattern = re.compile(rf"__OZPAY_{re.escape(token)}__:(\d+):(\d+)__")
    results: list[Optional[dict]] = [None] * len(commands)
    pos = 0
    for match in pattern.finditer(text):
        idx = int(match.group(1))
        code = int(match.group(2))
        chunk = text[pos:match.start()]
        pos = match.end()
        if chunk.startswith("\n"):
            chunk = chunk[1:]
        if chunk.endswith("\n"):
            chunk = chunk[:-1]
        if 0 <= idx < len(commands) and results[idx] is None:
            results[idx] = {
                "command": commands[idx],
                "stdout": chunk,
                "stderr": "",
                "exit_code": code,
            }
    tail = text[pos:]
    first_missing = True
    for i, command in enumerate(commands):
        if results[i] is not None:
            continue
        results[i] = {
            "command": command,
            "stdout": tail if first_missing else "",
            "stderr": "",
            "exit_code": shell_exit if first_missing else None,
            "skipped": not first_missing,
        }
        if first_missing:
            tail = ""
            first_missing = False
    return [item for item in results if item is not None]


def _command_preview(command: str) -> str:
    text = (command or "").strip()
    if not text:
        return ""
    if text.startswith("cat "):
        if "redsocks.conf" in text:
            return "запись redsocks.conf"
        if "entrypoint.sh" in text:
            return "запись entrypoint.sh"
        if "docker-compose.yml" in text:
            return "запись docker-compose.yml"
        if "Dockerfile" in text:
            return "запись Dockerfile"
        if "redroid.conf" in text:
            return "запись modules-load redroid.conf"
        if "OZPAY_DOCKER_PS_START" in text:
            return "docker ps — проверка контейнеров"
    if text.strip() == "docker ps" or text.strip().startswith("docker ps "):
        return "docker ps — проверка контейнеров"
    first = text.split("\n", 1)[0]
    if len(first) > 160:
        return first[:157] + "..."
    return first


class _LiveSshPrinter:
    def __init__(self, commands: list[str], token: str, echo: bool):
        self.commands = commands
        self.echo = echo
        self.buf = ""
        self.start_re = re.compile(rf"^__OZPAY_START_{re.escape(token)}__:(\d+)__$")
        self.end_re = re.compile(rf"^__OZPAY_{re.escape(token)}__:(\d+):(\d+)__$")

    def feed(self, text: str) -> None:
        if not text:
            return
        self.buf += text.replace("\r\n", "\n").replace("\r", "\n")
        self._drain(False)

    def finish(self) -> None:
        self._drain(True)

    def _drain(self, flush: bool) -> None:
        while True:
            nl = self.buf.find("\n")
            if nl < 0:
                if flush and self.buf:
                    self._emit(self.buf)
                    self.buf = ""
                return
            line = self.buf[:nl]
            self.buf = self.buf[nl + 1 :]
            self._emit(line)

    def _emit(self, line: str) -> None:
        if not self.echo:
            return
        stripped = line.strip()
        start = self.start_re.fullmatch(stripped)
        if start:
            idx = int(start.group(1))
            command = self.commands[idx] if 0 <= idx < len(self.commands) else ""
            print(f"[ssh] $ {_command_preview(command)}", flush=True)
            return
        end = self.end_re.fullmatch(stripped)
        if end:
            print(f"[ssh] exit={end.group(2)}", flush=True)
            return
        print(line, flush=True)


def _stream_ssh_channel(channel, timeout: Optional[float], on_data) -> tuple[str, int]:
    deadline = (time.monotonic() + float(timeout)) if timeout else None
    chunks: list[bytes] = []

    def take(ready: bool, recv) -> bool:
        if not ready:
            return False
        data = recv(8192)
        if not data:
            return False
        chunks.append(data)
        on_data(data.decode("utf-8", errors="replace"))
        return True

    while True:
        if deadline is not None and time.monotonic() > deadline:
            try:
                channel.close()
            except Exception:
                pass
            raise TimeoutError(f"SSH-команды превысили таймаут {int(timeout)} с")
        got = take(channel.recv_ready(), channel.recv)
        got = take(channel.recv_stderr_ready(), channel.recv_stderr) or got
        if channel.exit_status_ready() and not channel.recv_ready() and not channel.recv_stderr_ready():
            take(True, channel.recv) if channel.recv_ready() else None
            while channel.recv_ready():
                take(True, channel.recv)
            while channel.recv_stderr_ready():
                take(True, channel.recv_stderr)
            break
        if not got:
            time.sleep(0.05)
    return b"".join(chunks).decode("utf-8", errors="replace"), channel.recv_exit_status()


def run_ssh_commands(
    device_name: str,
    commands,
    timeout: Optional[float] = None,
    echo: bool = True,
) -> list[dict]:
    """Выполнить команды в одной SSH-сессии (общий cwd, переменные, окружение)."""
    device_name = (device_name or "").strip()
    command_list = _normalize_commands(commands)
    if not command_list:
        raise ValueError("Команда обязательна")

    token = uuid.uuid4().hex
    status_var = f"__ozpay_st_{token[:8]}"
    script_lines = ["exec 2>&1"]
    for index, command in enumerate(command_list):
        script_lines.append(f'printf "\\n__OZPAY_START_{token}__:%s__\\n" "{index}"')
        script_lines.append(command)
        script_lines.append(f"{status_var}=$?")
        script_lines.append(
            f'printf "\\n__OZPAY_{token}__:%s:%s__\\n" "{index}" "${{{status_var}}}"'
        )
    script = "\n".join(script_lines) + "\n"

    printer = _LiveSshPrinter(command_list, token, echo)
    client, ip, port = _connect_host(device_name)
    if echo:
        print(f"[ssh] {device_name}@{ip}:{port} команд={len(command_list)}", flush=True)
    try:
        stdin, stdout, _stderr = client.exec_command("/bin/sh -s", timeout=timeout)
        stdin.write(script)
        stdin.flush()
        stdin.channel.shutdown_write()
        out, shell_exit = _stream_ssh_channel(stdout.channel, timeout, printer.feed)
        printer.finish()
    finally:
        client.close()

    results = _parse_session_output(out, token, command_list, shell_exit)
    last_code = results[-1].get("exit_code") if results else shell_exit
    if echo:
        print(f"[ssh] {device_name}@{ip}:{port} готово, последняя команда exit={last_code}", flush=True)
    else:
        print(f"[ssh] {device_name}@{ip}:{port} cmds={len(command_list)} exit={last_code}")
    return results


def run_ssh_command(
    device_name: str,
    command: str,
    timeout: Optional[float] = None,
    echo: bool = True,
) -> dict:
    """Выполнить команду по SSH на хосте девайса.

    Returns:
        dict с ключами stdout, stderr, exit_code.
    """
    results = run_ssh_commands(device_name, [command], timeout=timeout, echo=echo)
    return results[0]


if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("usage: ssh_cmd.py <device> <command>", file=sys.stderr)
        sys.exit(2)
    run_ssh_command(sys.argv[1], " ".join(sys.argv[2:]))