"""
RDP Manager - professional web tool for administering Remote Desktop access
on Windows machines you already control.

The tool connects to target machines over WinRM using administrator credentials
that YOU supply, then automates ordinary administrative tasks: enabling RDP,
provisioning local user accounts and recording the generated credentials in a
local database. It performs no scanning, brute-forcing or unauthorised access --
it can only manage hosts you already administer.

Security posture (see SECURITY.md):
  * App login password is salted+hashed with Werkzeug (never stored in clear).
  * Stored RDP passwords are encrypted at rest with Fernet (AES-128-CBC + HMAC).
  * Secrets (Flask secret key + encryption key) are read from the environment or
    generated once and persisted to the gitignored instance/ directory.
  * WinRM supports HTTPS transport (recommended) as well as plain HTTP.
"""

from __future__ import annotations

import csv
import io
import json
import logging
import os
import re
import secrets
import string
from datetime import datetime, timezone
from pathlib import Path

from cryptography.fernet import Fernet, InvalidToken
from flask import Flask, jsonify, render_template, request, send_file, session
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import check_password_hash, generate_password_hash

try:  # pywinrm is only needed for the parts that talk to real machines.
    from winrm.protocol import Protocol
except Exception:  # pragma: no cover - import guard for environments without pywinrm
    Protocol = None

# --------------------------------------------------------------------------- #
# Paths & configuration
# --------------------------------------------------------------------------- #

BASE_DIR = Path(__file__).resolve().parent
INSTANCE_DIR = Path(os.environ.get("RDP_INSTANCE_DIR", BASE_DIR / "instance"))
INSTANCE_DIR.mkdir(parents=True, exist_ok=True)
REPORTS_DIR = INSTANCE_DIR / "reports"
REPORTS_DIR.mkdir(parents=True, exist_ok=True)

# Load a local .env (if present) so operators can set the login password and
# other options by editing a simple file. Existing environment variables win.
try:
    from dotenv import load_dotenv

    load_dotenv(BASE_DIR / ".env")
except Exception:  # pragma: no cover - dotenv optional
    pass

MACHINE_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]{1,64}$")
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
DEFAULT_ADMIN_EMAIL = "admin@example.com"
MAX_BULK_USERS = 50

logging.basicConfig(
    level=os.environ.get("LOG_LEVEL", "INFO"),
    format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("rdp-manager")


def _persisted_secret(filename: str, generator) -> str:
    """Return a secret from the instance dir, generating+persisting it once."""
    path = INSTANCE_DIR / filename
    if path.exists():
        return path.read_text().strip()
    value = generator()
    path.write_text(value)
    try:
        path.chmod(0o600)
    except OSError:  # pragma: no cover - non-POSIX filesystems
        pass
    log.warning("Generated new %s at %s (keep this file safe).", filename, path)
    return value


def _get_secret_key() -> str:
    return os.environ.get("SECRET_KEY") or _persisted_secret(
        "flask_secret.key", lambda: secrets.token_urlsafe(48)
    )


def _get_encryption_key() -> bytes:
    key = os.environ.get("RDP_ENCRYPTION_KEY") or _persisted_secret(
        "encryption.key", lambda: Fernet.generate_key().decode()
    )
    return key.encode() if isinstance(key, str) else key


app = Flask(__name__)
app.config.update(
    SECRET_KEY=_get_secret_key(),
    SQLALCHEMY_DATABASE_URI=os.environ.get(
        "DATABASE_URL", f"sqlite:///{INSTANCE_DIR / 'rdp_manager.db'}"
    ),
    SQLALCHEMY_TRACK_MODIFICATIONS=False,
    PERMANENT_SESSION_LIFETIME=int(os.environ.get("SESSION_LIFETIME_DAYS", "7")) * 86400,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
    # Set SESSION_COOKIE_SECURE=1 when serving over HTTPS.
    SESSION_COOKIE_SECURE=os.environ.get("SESSION_COOKIE_SECURE", "0") == "1",
)

db = SQLAlchemy(app)
_fernet = Fernet(_get_encryption_key())


def encrypt_secret(plaintext: str) -> str:
    """Encrypt a secret for storage at rest."""
    return _fernet.encrypt(plaintext.encode()).decode()


def decrypt_secret(token: str) -> str:
    """Decrypt a stored secret, tolerating legacy plaintext values."""
    if not token:
        return ""
    try:
        return _fernet.decrypt(token.encode()).decode()
    except (InvalidToken, ValueError):
        # Value predates encryption (or key changed) -- return as-is.
        return token


# --------------------------------------------------------------------------- #
# Database models
# --------------------------------------------------------------------------- #


class Settings(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    admin_email = db.Column(db.String(255))
    password_hash = db.Column(db.String(256), nullable=False)
    created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))


class Machine(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), unique=True, nullable=False)
    host = db.Column(db.String(255), nullable=False)
    admin_username = db.Column(db.String(100), nullable=False)
    description = db.Column(db.String(500))
    port = db.Column(db.Integer)  # None -> derived from use_https
    use_https = db.Column(db.Boolean, default=False)
    verify_cert = db.Column(db.Boolean, default=True)
    created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))

    rdp_users = db.relationship(
        "RDPUser", backref="machine", lazy=True, cascade="all, delete-orphan"
    )

    @property
    def winrm_port(self) -> int:
        if self.port:
            return self.port
        return 5986 if self.use_https else 5985

    @property
    def winrm_endpoint(self) -> str:
        scheme = "https" if self.use_https else "http"
        return f"{scheme}://{self.host}:{self.winrm_port}/wsman"


class RDPUser(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    machine_id = db.Column(db.Integer, db.ForeignKey("machine.id"), nullable=False)
    rdp_username = db.Column(db.String(100), nullable=False)
    rdp_password = db.Column(db.Text, nullable=False)  # encrypted at rest
    rdp_enabled = db.Column(db.Boolean, default=False)
    nla_enabled = db.Column(db.Boolean, default=False)
    firewall_status = db.Column(db.String(50), default="UNKNOWN")
    status = db.Column(db.String(50), default="PENDING")  # PENDING/SUCCESS/PARTIAL/FAILED
    error_message = db.Column(db.String(500))
    created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
    configured_at = db.Column(db.DateTime)


# --------------------------------------------------------------------------- #
# Password generation
# --------------------------------------------------------------------------- #

# Excludes quotes, backticks and $ so values are safe inside single-quoted
# PowerShell strings (see _ps_quote).
_PW_SPECIAL = "!@#%^&*-_=+"


def generate_password(length: int = 24) -> str:
    """Generate a cryptographically secure password with mixed character types."""
    length = max(12, length)
    pools = [string.ascii_uppercase, string.ascii_lowercase, string.digits, _PW_SPECIAL]
    all_chars = "".join(pools)
    chars = [secrets.choice(pool) for pool in pools]
    chars += [secrets.choice(all_chars) for _ in range(length - len(pools))]
    secrets.SystemRandom().shuffle(chars)
    return "".join(chars)


# --------------------------------------------------------------------------- #
# WinRM helpers
# --------------------------------------------------------------------------- #


def _ps_quote(value: str) -> str:
    """Safely embed a value in a single-quoted PowerShell string."""
    return "'" + value.replace("'", "''") + "'"


def _build_protocol(machine: "Machine", password: str):
    if Protocol is None:  # pragma: no cover
        raise RuntimeError("pywinrm is not installed; cannot reach remote machines.")
    return Protocol(
        endpoint=machine.winrm_endpoint,
        transport="ntlm",
        username=machine.admin_username,
        password=password,
        server_cert_validation="validate" if machine.verify_cert else "ignore",
    )


def connect_machine(machine: "Machine", password: str):
    """Open and close a WinRM shell to verify connectivity/credentials."""
    try:
        protocol = _build_protocol(machine, password)
        shell_id = protocol.open_shell()
        protocol.close_shell(shell_id)
        return True, "Connected"
    except Exception as exc:  # noqa: BLE001 - surface any transport error to the UI
        return False, str(exc)


def execute_ps(machine: "Machine", password: str, command: str):
    """Run a PowerShell command on the machine over WinRM."""
    try:
        protocol = _build_protocol(machine, password)
        shell_id = protocol.open_shell()
        # Wrap in powershell.exe so we can pass script text verbatim.
        command_id = protocol.run_command(shell_id, "powershell", ["-NoProfile", "-Command", command])
        stdout, stderr, rc = protocol.get_command_output(shell_id, command_id)
        protocol.cleanup_command(shell_id, command_id)
        protocol.close_shell(shell_id)
        decode = lambda b: b.decode(errors="replace") if isinstance(b, bytes) else str(b)
        return rc == 0, decode(stdout), decode(stderr)
    except Exception as exc:  # noqa: BLE001
        return False, "", str(exc)


def ps_enable_rdp() -> str:
    return (
        "Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' "
        "-Name 'fDenyTSConnections' -Value 0; Write-Output 'RDP_ENABLED'"
    )


def ps_enable_nla() -> str:
    return (
        "Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server"
        "\\WinStations\\RDP-Tcp' -Name 'UserAuthentication' -Value 1; Write-Output 'NLA_ENABLED'"
    )


def ps_start_service() -> str:
    return (
        "Set-Service -Name TermService -StartupType Automatic; "
        "Start-Service -Name TermService; Write-Output 'SERVICE_STARTED'"
    )


def ps_firewall() -> str:
    return (
        "Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue; "
        "if (-not (Get-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue)) "
        "{ New-NetFirewallRule -DisplayName 'Remote Desktop' -Direction Inbound -Protocol TCP "
        "-LocalPort 3389 -Action Allow -Profile Any }; Write-Output 'FIREWALL_ENABLED'"
    )


def ps_create_user(username: str, password: str) -> str:
    u, p = _ps_quote(username), _ps_quote(password)
    return (
        f"$u = {u}; $p = ConvertTo-SecureString {p} -AsPlainText -Force; "
        "$existing = Get-LocalUser -Name $u -ErrorAction SilentlyContinue; "
        "if ($existing) { Set-LocalUser -Name $u -Password $p; Enable-LocalUser -Name $u; "
        "Write-Output 'USER_UPDATED' } "
        "else { New-LocalUser -Name $u -Password $p -PasswordNeverExpires -ErrorAction Stop; "
        "Write-Output 'USER_CREATED' }"
    )


def ps_add_to_group(username: str) -> str:
    u = _ps_quote(username)
    return (
        f"$u = {u}; "
        "if (-not (Get-LocalGroupMember -Group 'Remote Desktop Users' -Member $u -ErrorAction SilentlyContinue)) "
        "{ Add-LocalGroupMember -Group 'Remote Desktop Users' -Member $u }; "
        "Write-Output 'GROUP_ADDED'"
    )


def _next_user_index(machine: "Machine") -> int:
    existing = {u.rdp_username for u in machine.rdp_users}
    i = 1
    while f"rdp-{machine.name}-{i}" in existing:
        i += 1
    return i


def provision_users(machine: "Machine", admin_password: str, count: int):
    """Enable RDP on the machine (once) and create `count` RDP users."""
    count = max(1, min(count, MAX_BULK_USERS))
    setup_errors = []

    if not machine.rdp_users:  # one-time host setup
        for step_name, cmd in (
            ("enable RDP", ps_enable_rdp()),
            ("enable NLA", ps_enable_nla()),
            ("start service", ps_start_service()),
            ("firewall", ps_firewall()),
        ):
            ok, _out, err = execute_ps(machine, admin_password, cmd)
            if not ok:
                setup_errors.append(f"{step_name}: {err.strip()}")

    created, failures = [], []
    start = _next_user_index(machine)
    for i in range(start, start + count):
        username = f"rdp-{machine.name}-{i}"
        password = generate_password()
        ok, _out, err = execute_ps(machine, admin_password, ps_create_user(username, password))
        if not ok:
            failures.append(f"{username}: {err.strip()}")
            continue
        execute_ps(machine, admin_password, ps_add_to_group(username))

        user = RDPUser(
            machine_id=machine.id,
            rdp_username=username,
            rdp_password=encrypt_secret(password),
            rdp_enabled=True,
            nla_enabled=True,
            firewall_status="ENABLED",
            status="SUCCESS" if not setup_errors else "PARTIAL",
            error_message="; ".join(setup_errors) or None,
            configured_at=datetime.now(timezone.utc),
        )
        db.session.add(user)
        created.append({"username": username, "password": password})

    db.session.commit()
    return created, setup_errors + failures


# --------------------------------------------------------------------------- #
# Authentication
# --------------------------------------------------------------------------- #


def _ensure_settings() -> "Settings":
    """Return app settings, initialising the login password on first run."""
    settings = Settings.query.first()
    if settings:
        return settings
    email = (os.environ.get("RDP_MANAGER_EMAIL") or DEFAULT_ADMIN_EMAIL).strip().lower()
    initial = os.environ.get("RDP_MANAGER_PASSWORD")
    generated = False
    if not initial:
        initial = generate_password(16)
        generated = True
    settings = Settings(admin_email=email, password_hash=generate_password_hash(initial))
    db.session.add(settings)
    db.session.commit()
    if generated:
        # Do not print the secret to the log stream (captured by hosting log
        # aggregation). Write it to a 0600 file the operator can read instead.
        pw_file = INSTANCE_DIR / "INITIAL_PASSWORD.txt"
        try:
            pw_file.write_text(f"email: {email}\npassword: {initial}\n")
            pw_file.chmod(0o600)
            location = str(pw_file)
        except OSError:  # pragma: no cover - non-POSIX / read-only fs
            location = None
        log.warning("=" * 68)
        log.warning("First run: login email is %s and a random password was generated.", email)
        if location:
            log.warning("Both were written to: %s", location)
            log.warning("Read that file to log in, then change them in the UI and")
            log.warning("delete the file. Better: set RDP_MANAGER_EMAIL / RDP_MANAGER_PASSWORD.")
        else:
            log.warning("Set RDP_MANAGER_EMAIL / RDP_MANAGER_PASSWORD and restart.")
        log.warning("=" * 68)
    return settings


def _sync_env_password():
    """Make RDP_MANAGER_EMAIL / RDP_MANAGER_PASSWORD authoritative at startup.

    Runs at startup only, so editing .env (or the env vars) and restarting
    reliably changes the login email/password. Values not set via env keep the
    stored value (a UI change, or the first-run generated one).
    """
    settings = Settings.query.first()
    if not settings:
        return
    changed = False
    env_pw = os.environ.get("RDP_MANAGER_PASSWORD")
    if env_pw and not check_password_hash(settings.password_hash, env_pw):
        settings.password_hash = generate_password_hash(env_pw)
        changed = True
        log.info("Login password synced from RDP_MANAGER_PASSWORD.")
    env_email = os.environ.get("RDP_MANAGER_EMAIL")
    if env_email and (settings.admin_email or "").lower() != env_email.strip().lower():
        settings.admin_email = env_email.strip().lower()
        changed = True
        log.info("Login email synced from RDP_MANAGER_EMAIL.")
    if changed:
        db.session.commit()


def is_authenticated() -> bool:
    return bool(session.get("authenticated"))


def require_auth():
    if not is_authenticated():
        return jsonify({"success": False, "message": "Not authenticated"}), 401
    return None


# --------------------------------------------------------------------------- #
# Routes
# --------------------------------------------------------------------------- #


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/api/login", methods=["POST"])
def login():
    data = request.get_json(silent=True) or {}
    email = (data.get("email") or "").strip().lower()
    password = data.get("password", "")
    if not email or not password:
        return jsonify({"success": False, "message": "Email and password required"}), 400
    settings = _ensure_settings()
    stored_email = (settings.admin_email or DEFAULT_ADMIN_EMAIL).lower()
    if email == stored_email and check_password_hash(settings.password_hash, password):
        session["authenticated"] = True
        session.permanent = True
        return jsonify({"success": True, "message": "Logged in successfully"})
    return jsonify({"success": False, "message": "Invalid email or password"}), 401


@app.route("/api/logout", methods=["POST"])
def logout():
    session.pop("authenticated", None)
    return jsonify({"success": True})


@app.route("/api/me", methods=["GET"])
def me():
    guard = require_auth()
    if guard:
        return guard
    settings = _ensure_settings()
    return jsonify({"email": settings.admin_email or DEFAULT_ADMIN_EMAIL})


@app.route("/api/change-password", methods=["POST"])
def change_password():
    """Update the login email and/or password (current password required)."""
    guard = require_auth()
    if guard:
        return guard
    data = request.get_json(silent=True) or {}
    current = data.get("current_password", "")
    new_pw = data.get("new_password", "")
    new_email = (data.get("new_email") or "").strip().lower()
    settings = _ensure_settings()
    if not check_password_hash(settings.password_hash, current):
        return jsonify({"success": False, "message": "Current password is incorrect"}), 401
    if not new_pw and not new_email:
        return jsonify({"success": False, "message": "Provide a new email and/or a new password"}), 400
    if new_email and not EMAIL_RE.match(new_email):
        return jsonify({"success": False, "message": "Enter a valid email address"}), 400
    if new_pw and len(new_pw) < 8:
        return jsonify({"success": False, "message": "New password must be at least 8 characters"}), 400
    if new_email:
        settings.admin_email = new_email
    if new_pw:
        settings.password_hash = generate_password_hash(new_pw)
    db.session.commit()
    return jsonify({"success": True, "message": "Account updated", "email": settings.admin_email})


@app.route("/api/machines", methods=["GET"])
def get_machines():
    guard = require_auth()
    if guard:
        return guard
    result = []
    for m in Machine.query.order_by(Machine.created_at.desc()).all():
        users = m.rdp_users
        last = users[-1] if users else None
        result.append(
            {
                "id": m.id,
                "name": m.name,
                "host": m.host,
                "admin_username": m.admin_username,
                "description": m.description,
                "port": m.winrm_port,
                "use_https": m.use_https,
                "rdp_user_count": len(users),
                "status": last.status if last else "NOT_CONFIGURED",
                "created_at": m.created_at.strftime("%Y-%m-%d %H:%M"),
            }
        )
    return jsonify(result)


@app.route("/api/machines", methods=["POST"])
def add_machine():
    guard = require_auth()
    if guard:
        return guard
    data = request.get_json(silent=True) or {}
    name = (data.get("name") or "").strip()
    host = (data.get("host") or "").strip()
    admin_username = (data.get("admin_username") or "").strip()
    if not (name and host and admin_username):
        return jsonify({"success": False, "message": "name, host and admin_username are required"}), 400
    if not MACHINE_NAME_RE.match(name):
        return jsonify(
            {"success": False, "message": "Name may only contain letters, numbers, dot, dash, underscore"}
        ), 400
    if Machine.query.filter_by(name=name).first():
        return jsonify({"success": False, "message": "A machine with that name already exists"}), 409
    try:
        machine = Machine(
            name=name,
            host=host,
            admin_username=admin_username,
            description=(data.get("description") or "").strip(),
            port=int(data["port"]) if data.get("port") else None,
            use_https=bool(data.get("use_https", False)),
            verify_cert=bool(data.get("verify_cert", True)),
        )
        db.session.add(machine)
        db.session.commit()
        return jsonify({"success": True, "message": "Machine added", "id": machine.id}), 201
    except Exception as exc:  # noqa: BLE001
        db.session.rollback()
        return jsonify({"success": False, "message": str(exc)}), 400


@app.route("/api/machines/<int:machine_id>", methods=["DELETE"])
def delete_machine(machine_id):
    guard = require_auth()
    if guard:
        return guard
    machine = db.session.get(Machine, machine_id)
    if not machine:
        return jsonify({"success": False, "message": "Machine not found"}), 404
    db.session.delete(machine)
    db.session.commit()
    return jsonify({"success": True, "message": "Machine deleted"})


@app.route("/api/configure/<int:machine_id>", methods=["POST"])
def configure_machine(machine_id):
    guard = require_auth()
    if guard:
        return guard
    data = request.get_json(silent=True) or {}
    machine = db.session.get(Machine, machine_id)
    if not machine:
        return jsonify({"success": False, "message": "Machine not found"}), 404
    admin_password = data.get("admin_password") or ""
    if not admin_password:
        return jsonify({"success": False, "message": "Admin password required"}), 400
    created, errors = provision_users(machine, admin_password, 1)
    if not created:
        return jsonify({"success": False, "message": "; ".join(errors) or "Provisioning failed"}), 502
    return jsonify(
        {
            "success": not errors,
            "message": "Configured successfully" if not errors else "Configured with warnings",
            "rdp_username": created[0]["username"],
            "rdp_password": created[0]["password"],
            "errors": errors,
        }
    )


@app.route("/api/bulk-configure/<int:machine_id>", methods=["POST"])
def bulk_configure_machine(machine_id):
    guard = require_auth()
    if guard:
        return guard
    data = request.get_json(silent=True) or {}
    machine = db.session.get(Machine, machine_id)
    if not machine:
        return jsonify({"success": False, "message": "Machine not found"}), 404
    admin_password = data.get("admin_password") or ""
    if not admin_password:
        return jsonify({"success": False, "message": "Admin password required"}), 400
    try:
        count = int(data.get("count", 1))
    except (TypeError, ValueError):
        return jsonify({"success": False, "message": "count must be a number"}), 400
    if count < 1 or count > MAX_BULK_USERS:
        return jsonify({"success": False, "message": f"count must be between 1 and {MAX_BULK_USERS}"}), 400
    created, errors = provision_users(machine, admin_password, count)
    return jsonify(
        {
            "success": bool(created) and not errors,
            "message": f"Created {len(created)} RDP user(s)",
            "created_users": created,
            "errors": errors,
        }
    )


@app.route("/api/credentials/<int:machine_id>", methods=["GET"])
def get_credentials(machine_id):
    guard = require_auth()
    if guard:
        return guard
    machine = db.session.get(Machine, machine_id)
    if not machine:
        return jsonify({"success": False, "message": "Machine not found"}), 404
    users = [
        {
            "id": u.id,
            "rdp_username": u.rdp_username,
            "rdp_password": decrypt_secret(u.rdp_password),
            "rdp_enabled": u.rdp_enabled,
            "nla_enabled": u.nla_enabled,
            "status": u.status,
            "created_at": u.created_at.replace(tzinfo=timezone.utc).isoformat(),
            "configured_at": u.configured_at.strftime("%Y-%m-%d %H:%M") if u.configured_at else "Not configured",
        }
        for u in machine.rdp_users
    ]
    return jsonify(
        {
            "success": True,
            "machine_name": machine.name,
            "ip": machine.host,
            "users": users,
            "total_users": len(users),
        }
    )


def _all_rows():
    for m in Machine.query.all():
        if not m.rdp_users:
            yield [m.name, m.host, m.admin_username, "-", "-", "NOT_CONFIGURED", m.created_at.strftime("%Y-%m-%d %H:%M")]
        for u in m.rdp_users:
            yield [
                m.name,
                m.host,
                m.admin_username,
                u.rdp_username,
                decrypt_secret(u.rdp_password),
                u.status,
                u.created_at.strftime("%Y-%m-%d %H:%M"),
            ]


@app.route("/api/export-csv", methods=["GET"])
def export_csv():
    guard = require_auth()
    if guard:
        return guard
    buffer = io.StringIO()
    writer = csv.writer(buffer)
    writer.writerow(["Machine", "Host", "Admin User", "RDP Username", "RDP Password", "Status", "Created"])
    for row in _all_rows():
        writer.writerow(row)
    data = io.BytesIO(buffer.getvalue().encode())
    filename = f"rdp_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
    return send_file(data, mimetype="text/csv", as_attachment=True, download_name=filename)


@app.route("/api/export-json", methods=["GET"])
def export_json():
    guard = require_auth()
    if guard:
        return guard
    payload = {"exported_at": datetime.now(timezone.utc).isoformat(), "machines": []}
    for m in Machine.query.all():
        payload["machines"].append(
            {
                "name": m.name,
                "host": m.host,
                "admin_username": m.admin_username,
                "description": m.description,
                "users": [
                    {
                        "rdp_username": u.rdp_username,
                        "rdp_password": decrypt_secret(u.rdp_password),
                        "status": u.status,
                        "created_at": u.created_at.replace(tzinfo=timezone.utc).isoformat(),
                    }
                    for u in m.rdp_users
                ],
            }
        )
    data = io.BytesIO(json.dumps(payload, indent=2).encode())
    filename = f"rdp_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
    return send_file(data, mimetype="application/json", as_attachment=True, download_name=filename)


@app.route("/api/test-connection", methods=["POST"])
def test_connection():
    guard = require_auth()
    if guard:
        return guard
    data = request.get_json(silent=True) or {}
    machine_id = data.get("machine_id")
    if machine_id:
        machine = db.session.get(Machine, int(machine_id))
        if not machine:
            return jsonify({"success": False, "message": "Machine not found"}), 404
    else:
        # Ad-hoc connectivity check for a not-yet-saved machine.
        machine = Machine(
            name="adhoc",
            host=data.get("host", ""),
            admin_username=data.get("admin_username", ""),
            use_https=bool(data.get("use_https", False)),
            verify_cert=bool(data.get("verify_cert", True)),
            port=int(data["port"]) if data.get("port") else None,
        )
    ok, message = connect_machine(machine, data.get("admin_password", ""))
    return jsonify({"success": ok, "message": message})


def create_app():
    with app.app_context():
        db.create_all()
        _ensure_settings()
        _sync_env_password()
    return app


if __name__ == "__main__":
    create_app()
    host = os.environ.get("HOST", "127.0.0.1")
    port = int(os.environ.get("PORT", "5000"))
    debug = os.environ.get("FLASK_DEBUG", "0") == "1"
    print("\n" + "=" * 60)
    print("  RDP MANAGER - Professional Tool")
    print("=" * 60)
    print(f"  Open your browser at: http://{host}:{port}")
    print("  (first run writes the login password to instance/INITIAL_PASSWORD.txt)")
    print("  Press Ctrl+C to stop.\n")
    app.run(host=host, port=port, debug=debug)
