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

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

from __future__ import annotations

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

import paramiko

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


def get_conn():
    Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
    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 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(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"

    client, ip, port = _connect_host(device_name)
    try:
        stdin, stdout, stderr = client.exec_command("/bin/sh -s", timeout=timeout)
        stdin.write(script)
        stdin.flush()
        stdin.channel.shutdown_write()
        out = stdout.read().decode("utf-8", errors="replace")
        err = stderr.read().decode("utf-8", errors="replace")
        shell_exit = stdout.channel.recv_exit_status()
    finally:
        client.close()

    results = _parse_session_output(out, token, command_list, shell_exit)
    if err and results:
        last = results[-1]
        last["stderr"] = (last.get("stderr") or "") + err

    if echo:
        for item in results:
            print(f"$ {item['command']}")
            text = item.get("stdout") or ""
            if text:
                print(text, end="" if text.endswith("\n") else "\n")
            extra = item.get("stderr") or ""
            if extra:
                print(extra, end="" if extra.endswith("\n") else "\n", file=sys.stderr)
            if item.get("skipped"):
                print("[ssh] skipped")
            else:
                print(f"[ssh] exit={item.get('exit_code')}")

    last_code = results[-1].get("exit_code") if results else shell_exit
    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:]))