#!/usr/bin/env python3
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.

"""
SecuBox-Deb :: usersctl
CyberMind — https://cybermind.fr

usersctl — SecuBox Unified User Management CLI.

Thin wrapper over secubox_users.api.engine. Every mutation goes through the
same engine the FastAPI handlers use, so CLI ↔ API parity is mechanical.
"""
from __future__ import annotations

import argparse
import getpass
import json
import os
import socket
import sys
from pathlib import Path

# Ensure the engine import resolves whether we're installed (/usr/lib/...) or in-tree.
# In production, /usr/sbin/usersctl has only 2 parents; in-tree, parents[3] is the repo root.
sys.path.insert(0, "/usr/lib/secubox/users")
_here = Path(__file__).resolve()
try:
    sys.path.insert(0, str(_here.parents[1]))
except IndexError:
    pass
try:
    _common = _here.parents[3] / "common"
    if _common.exists():
        sys.path.insert(0, str(_common))
except IndexError:
    pass
# Production: secubox_core installed as a site-package, importable directly.

from api import engine, password_policy, totp, migrate_v1_to_v2  # noqa: E402

USERS_FILE = Path(os.environ.get("USERS_FILE", "/etc/secubox/users.json"))
AUTH_TOML_PATH = Path(os.environ.get("AUTH_TOML_PATH", "/etc/secubox/auth.toml"))


def _engine() -> engine.Engine:
    return engine.Engine(users_path=USERS_FILE)


def cmd_list(args):
    rows = _engine().list_users()
    if not rows:
        print("(no users)")
        return 0
    fmt = "{:<16} {:<28} {:<10} {:<10}"
    print(fmt.format("USERNAME", "EMAIL", "ROLE", "STATUS"))
    for u in rows:
        print(fmt.format(
            u["username"],
            u.get("email") or "-",
            u.get("role", "-"),
            "enabled" if u.get("enabled") else "disabled",
        ))
    return 0


def cmd_show(args):
    u = _engine().get_user(args.username)
    if not u:
        print(f"user not found: {args.username}", file=sys.stderr)
        return 2
    print(json.dumps(u, indent=2))
    return 0


def cmd_add(args):
    _engine().create_user(args.username, email=args.email, role=args.role)
    print(f"created: {args.username}")
    return 0


def cmd_set_password(args):
    plain = getpass.getpass("New password: ")
    confirm = getpass.getpass("Confirm: ")
    if plain != confirm:
        print("passwords do not match", file=sys.stderr)
        return 2
    _engine().set_password(args.username, plain)
    print(f"password set: {args.username}")
    return 0


def cmd_clear_password(args):
    _engine().clear_password(args.username)
    print(f"password cleared, must_change_password=true: {args.username}")
    return 0


def cmd_enable(args):
    _engine().enable_user(args.username)
    print(f"enabled: {args.username}")
    return 0


def cmd_disable(args):
    n = _engine().disable_user(args.username)
    print(f"disabled: {args.username} (revoked {n} session(s))")
    return 0


def cmd_totp_enroll(args):
    eng = _engine()
    secret = totp.generate_secret()
    backup = eng.enroll_totp(args.username, secret)
    host = socket.gethostname().strip() or "secubox"
    issuer = f"SecuBox · {host}"
    print("Scan this URI / secret with your authenticator app:")
    print(f"  Secret: {secret}")
    print(f"  URI:    {totp.provisioning_uri(args.username, secret, issuer=issuer)}")
    print("Backup codes (save them now — shown only once):")
    for c in backup:
        print(f"  {c}")
    return 0


def cmd_totp_disable(args):
    _engine().disable_totp(args.username)
    print(f"totp disabled: {args.username}")
    return 0


def cmd_totp_backup_codes(args):
    new = _engine().regenerate_backup_codes(args.username)
    print("New backup codes (old ones invalidated):")
    for c in new:
        print(f"  {c}")
    return 0


def cmd_sessions(args):
    sess_path = Path(os.environ.get("SECUBOX_AUTH_SESSIONS", "/var/lib/secubox/auth/sessions.json"))
    rows = json.loads(sess_path.read_text() or "[]") if sess_path.exists() else []
    rows = [r for r in rows if r.get("username") == args.username]
    print(json.dumps(rows, indent=2))
    return 0


def cmd_revoke(args):
    n = _engine().revoke_sessions(args.username)
    print(f"revoked {n} session(s) for {args.username}")
    return 0


def cmd_migrate(args):
    migrate_v1_to_v2.migrate(USERS_FILE, auth_toml_path=AUTH_TOML_PATH)
    print("migration complete")
    return 0


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="usersctl", description="SecuBox user management CLI")
    sub = p.add_subparsers(dest="cmd")
    sub.required = True

    sub.add_parser("list", help="List all users").set_defaults(func=cmd_list)

    sp = sub.add_parser("show", help="Show one user as JSON")
    sp.add_argument("username")
    sp.set_defaults(func=cmd_show)

    sp = sub.add_parser("add", help="Create a user")
    sp.add_argument("username")
    sp.add_argument("--email", required=True)
    sp.add_argument("--role", required=True, choices=["admin", "operator", "viewer"])
    sp.set_defaults(func=cmd_add)

    sp = sub.add_parser("set-password", help="Set a user's password (prompts)")
    sp.add_argument("username")
    sp.set_defaults(func=cmd_set_password)

    sp = sub.add_parser("clear-password", help="Reset to must_change_password=true")
    sp.add_argument("username")
    sp.set_defaults(func=cmd_clear_password)

    sp = sub.add_parser("enable", help="Enable a user")
    sp.add_argument("username")
    sp.set_defaults(func=cmd_enable)

    sp = sub.add_parser("disable", help="Disable a user (also revokes sessions)")
    sp.add_argument("username")
    sp.set_defaults(func=cmd_disable)

    sp = sub.add_parser("totp-enroll", help="Enroll TOTP for a user (admin path)")
    sp.add_argument("username")
    sp.set_defaults(func=cmd_totp_enroll)

    sp = sub.add_parser("totp-disable", help="Disable TOTP for a user (admin)")
    sp.add_argument("username")
    sp.set_defaults(func=cmd_totp_disable)

    sp = sub.add_parser("totp-backup-codes", help="Regenerate backup codes")
    sp.add_argument("username")
    sp.set_defaults(func=cmd_totp_backup_codes)

    sp = sub.add_parser("sessions", help="List active sessions for a user")
    sp.add_argument("username")
    sp.set_defaults(func=cmd_sessions)

    sp = sub.add_parser("revoke", help="Force-logout (without disabling)")
    sp.add_argument("username")
    sp.set_defaults(func=cmd_revoke)

    sub.add_parser("migrate-v1-to-v2", help="Migrate users.json + auth.toml to v2").set_defaults(func=cmd_migrate)

    return p


def main(argv=None) -> int:
    args = build_parser().parse_args(argv)
    try:
        return args.func(args)
    except engine.EngineError as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 2
    except password_policy.PolicyError as exc:
        print(f"error: {exc}", file=sys.stderr)
        print(
            "Policy: 12-128 chars, ≥3 of {lower, upper, digit, symbol}, "
            "no username substring, not in common-passwords list.",
            file=sys.stderr,
        )
        return 2


if __name__ == "__main__":
    sys.exit(main())
