#!/usr/bin/env python3
"""CLI for the `seafile` skill — read-only Seafile Pro (SeaCloud/SeaDrive) inventory
for the GPS backup audit.

Commands:
  status                     server reachable + version
  users [--inactive] [--json]      all accounts + storage used + last login
  libraries [--json]         all libraries (repos) + owner + size
  usage [--json]             per-user rollup: storage + library count (the audit view)
  devices [--user EMAIL] [--json]  desktop/sync devices the server has seen
  audit [--client NAME] [--rmm] [--json]
                             the GPS view: users/usage, optionally cross-checked
                             against GuruRMM endpoints (SeaDrive/Seafile installed?)

Write ops (admin) -- ALL preview by default; add --confirm to apply:
  user-create EMAIL --password P [--name N] [--quota-gb G] [--role R] [--inactive]
  user-set EMAIL [--active|--inactive] [--quota-gb G] [--name N] [--role R] [--password P]
  quota EMAIL --gb G                     set storage quota (decimal GB, as shown by `users`)
  user-delete EMAIL                      remove account (irreversible)
  lib-create NAME [--owner EMAIL]        create a library
  lib-transfer REPO_ID --to EMAIL        transfer ownership
  lib-delete REPO_ID                     delete a library (irreversible)
  share REPO_ID --to EMAIL... [--perm r|rw] [--group]
  unshare REPO_ID --to EMAIL [--group]

Server-side is the source of "who has an account + how much data"; --rmm adds the
"which enrolled machine actually runs the client" half (the 'both' cross-check).
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from seafile_client import SeafileClient  # noqa: E402
import backup_common as bc  # noqa: E402

GB = 1_000_000_000


def _gb(n) -> float:
    try:
        return round(int(n) / GB, 2)
    except (TypeError, ValueError):
        return 0.0


def _fmt_quota(total) -> str:
    try:
        t = int(total)
    except (TypeError, ValueError):
        return "?"
    return "unlimited" if t < 0 else f"{_gb(t)} GB"


def cmd_status(c: SeafileClient, args) -> int:
    info = c.server_info()
    print(f"[OK] {c.base}")
    print(json.dumps(info, indent=2))
    return 0


def cmd_users(c: SeafileClient, args) -> int:
    users = c.list_users()
    if args.inactive:
        users = [u for u in users if not u.get("is_active")]
    users.sort(key=lambda u: int(u.get("quota_usage") or 0), reverse=True)
    if args.json:
        print(json.dumps(users, indent=2))
        return 0
    print(f"{'EMAIL':40} {'ACTIVE':6} {'USED':>10} {'QUOTA':>10} {'LAST LOGIN':20}")
    for u in users:
        print(f"{(u.get('email') or '')[:40]:40} "
              f"{'yes' if u.get('is_active') else 'NO':6} "
              f"{_gb(u.get('quota_usage')):>7} GB "
              f"{_fmt_quota(u.get('quota_total')):>10} "
              f"{(u.get('last_login') or '-')[:19]:20}")
    print(f"\n{len(users)} users; total used "
          f"{round(sum(_gb(u.get('quota_usage')) for u in users), 1)} GB")
    return 0


def cmd_libraries(c: SeafileClient, args) -> int:
    repos = c.list_libraries()
    repos.sort(key=lambda r: int(r.get("size") or 0), reverse=True)
    if args.json:
        print(json.dumps(repos, indent=2))
        return 0
    print(f"{'LIBRARY':30} {'OWNER':35} {'SIZE':>10} {'FILES':>8} {'MODIFIED':20}")
    for r in repos:
        print(f"{(r.get('name') or '')[:30]:30} "
              f"{(r.get('owner_email') or r.get('owner') or '')[:35]:35} "
              f"{_gb(r.get('size')):>7} GB "
              f"{str(r.get('file_count') or '?'):>8} "
              f"{(r.get('last_modified') or '-')[:19]:20}")
    print(f"\n{len(repos)} libraries; total "
          f"{round(sum(_gb(r.get('size')) for r in repos), 1)} GB")
    return 0


def _usage_rollup(c: SeafileClient) -> list[dict]:
    users = {u.get("email"): u for u in c.list_users()}
    repos = c.list_libraries()
    by_owner: dict[str, dict] = {}
    for r in repos:
        owner = r.get("owner_email") or r.get("owner")
        d = by_owner.setdefault(owner, {"libraries": 0, "lib_bytes": 0})
        d["libraries"] += 1
        d["lib_bytes"] += int(r.get("size") or 0)
    rollup = []
    for email, u in users.items():
        o = by_owner.get(email, {"libraries": 0, "lib_bytes": 0})
        rollup.append({
            "email": email,
            "name": u.get("name"),
            "is_active": bool(u.get("is_active")),
            "quota_used_bytes": int(u.get("quota_usage") or 0),
            "quota_used_gb": _gb(u.get("quota_usage")),
            "quota_total": u.get("quota_total"),
            "libraries": o["libraries"],
            "library_gb": _gb(o["lib_bytes"]),
            "last_login": u.get("last_login"),
            "last_access": u.get("last_access_time"),
        })
    rollup.sort(key=lambda x: x["quota_used_bytes"], reverse=True)
    return rollup


def cmd_usage(c: SeafileClient, args) -> int:
    rollup = _usage_rollup(c)
    if args.json:
        print(json.dumps(rollup, indent=2))
        return 0
    print(f"{'USER':40} {'ACT':4} {'USED':>9} {'LIBS':>5} {'LAST LOGIN':20}")
    for r in rollup:
        print(f"{(r['email'] or '')[:40]:40} "
              f"{'yes' if r['is_active'] else 'NO':4} "
              f"{r['quota_used_gb']:>6} GB "
              f"{r['libraries']:>5} "
              f"{(r['last_login'] or '-')[:19]:20}")
    return 0


def cmd_devices(c: SeafileClient, args) -> int:
    devices = c.list_devices(args.user)
    if args.json:
        print(json.dumps(devices, indent=2))
        return 0
    if not devices:
        print("[INFO] no devices returned (endpoint may be unsupported on this Seafile version)")
        return 0
    print(f"{'USER':35} {'DEVICE':25} {'PLATFORM':12} {'LAST ACCESS':20}")
    for d in devices:
        print(f"{(d.get('user') or '')[:35]:35} "
              f"{(d.get('device_name') or '')[:25]:25} "
              f"{(d.get('platform') or '')[:12]:12} "
              f"{(d.get('last_accessed') or d.get('last_login_ip') or '-')[:19]:20}")
    return 0


def cmd_audit(c: SeafileClient, args) -> int:
    rollup = _usage_rollup(c)
    # active accounts with data are the ones actually backing up here
    active = [r for r in rollup if r["is_active"] and r["quota_used_bytes"] > 0]
    result = {"backend": "seafile", "server": c.base, "accounts": active}

    if args.rmm:
        rmm = bc.RmmClient()
        rmm.login()
        checks = []
        agents = rmm.find_agents(client_substr=args.client) if args.client else rmm.list_agents()
        for a in agents:
            det = rmm.detect_client(a, ["Seafile", "SeaDrive"])
            if det.get("detected"):
                checks.append(det)
        result["rmm_endpoints_with_client"] = checks

    if args.json:
        print(json.dumps(result, indent=2))
        return 0

    print(f"=== Seafile (SeaCloud/SeaDrive) backup accounts - {c.base} ===")
    print(f"{'USER':40} {'USED':>9} {'LIBS':>5} {'LAST LOGIN':20}")
    for r in active:
        print(f"{(r['email'] or '')[:40]:40} {r['quota_used_gb']:>6} GB "
              f"{r['libraries']:>5} {(r['last_login'] or '-')[:19]:20}")
    print(f"\n{len(active)} active accounts with data; "
          f"{round(sum(r['quota_used_gb'] for r in active), 1)} GB total")
    if args.rmm:
        eps = result.get("rmm_endpoints_with_client", [])
        print(f"\n--- GuruRMM endpoints running a Seafile/SeaDrive client ({len(eps)}) ---")
        for e in eps:
            print(f"  {e['hostname']:25} client={e.get('client_name')}")
    return 0


# --- write ops (gated) --------------------------------------------------------
# Quota units: Seafile's admin API takes quota_total in MB, where 1 MB == 1 MiB
# (get_file_size_unit('MB') == 2**20). This tool's read commands (users/usage/audit)
# render quota as decimal GB (bytes / 1e9, see _gb/_fmt_quota). To make a quota you
# SET round-trip with the quota you later SEE here, --gb is decimal GB and we convert
# to the stored MB count accordingly. (Note: Seafile's own web admin UI shows quota
# in binary GiB, so the same quota reads ~7% smaller there.)
MB_BYTES = 1 << 20            # Seafile's MB unit (1 MiB)
GB_BYTES = 1_000_000_000     # this tool's display GB (matches _gb)


def _gb_to_mb(gb) -> int:
    return int(round(float(gb) * GB_BYTES / MB_BYTES))


def _do_write(args, label: str, detail: dict, fn, reminder: str | None = None,
              on_result=None) -> int:
    """Print the plan; execute only with --confirm. Preview is fully offline.

    `on_result(result) -> int`: optional post-execute check that may print its own
    warnings and return a non-zero exit code (e.g. partial-success responses that
    still come back HTTP 200). A non-zero return suppresses the [OK] line.
    """
    print(f"[PLAN] {label}")
    for k, v in detail.items():
        print(f"    {k}: {v}")
    if not getattr(args, "confirm", False):
        print("[PREVIEW] not executed. Re-run with --confirm to apply.")
        return 0
    result = fn()
    if on_result is not None:
        rc = on_result(result)
        if rc:
            return rc
    print(f"[OK] {label}")
    if reminder:
        print(f"[REMINDER] {reminder}")
    if result:
        print(json.dumps(result, indent=2))
    return 0


def cmd_user_create(c: SeafileClient, args) -> int:
    quota_mb = _gb_to_mb(args.quota_gb) if args.quota_gb is not None else None
    detail = {"email": args.email, "active": not args.inactive, "password": "<set>"}
    if args.name:
        detail["name"] = args.name
    if quota_mb is not None:
        detail["quota"] = f"{args.quota_gb} GB (-> {quota_mb} MB stored)"
    if args.role:
        detail["role"] = args.role
    return _do_write(
        args, f"create user {args.email}", detail,
        lambda: c.create_user(args.email, args.password, name=args.name,
                              quota_mb=quota_mb, role=args.role,
                              is_active=not args.inactive),
        reminder="password is a credential -- store it in the SOPS vault (vault skill).")


def cmd_user_set(c: SeafileClient, args) -> int:
    if args.active and args.inactive:
        print("[ERROR] --active and --inactive are mutually exclusive", file=sys.stderr)
        return 2
    is_active = True if args.active else (False if args.inactive else None)
    quota_mb = _gb_to_mb(args.quota_gb) if args.quota_gb is not None else None
    detail: dict = {"email": args.email}
    if is_active is not None:
        detail["active"] = is_active
    if quota_mb is not None:
        detail["quota"] = f"{args.quota_gb} GB (-> {quota_mb} MB stored)"
    if args.name is not None:
        detail["name"] = args.name
    if args.role is not None:
        detail["role"] = args.role
    if args.password is not None:
        detail["password"] = "<set>"
    if len(detail) == 1:
        print("[ERROR] nothing to change (give --active/--inactive/--quota-gb/--name/--role/--password)",
              file=sys.stderr)
        return 2
    reminder = ("password is a credential -- store it in the SOPS vault (vault skill)."
                if args.password is not None else None)
    return _do_write(
        args, f"update user {args.email}", detail,
        lambda: c.update_user(args.email, is_active=is_active, password=args.password,
                              name=args.name, quota_mb=quota_mb, role=args.role),
        reminder=reminder)


def cmd_quota(c: SeafileClient, args) -> int:
    quota_mb = _gb_to_mb(args.gb)
    detail = {"email": args.email, "quota": f"{args.gb} GB (-> {quota_mb} MB stored)"}
    return _do_write(args, f"set quota for {args.email}", detail,
                     lambda: c.update_user(args.email, quota_mb=quota_mb))


def cmd_user_delete(c: SeafileClient, args) -> int:
    detail = {"email": args.email, "note": "IRREVERSIBLE - removes the account"}
    return _do_write(args, f"DELETE user {args.email}", detail,
                     lambda: c.delete_user(args.email))


def cmd_lib_create(c: SeafileClient, args) -> int:
    detail = {"name": args.name, "owner": args.owner or "<admin account>"}
    return _do_write(args, f"create library '{args.name}'", detail,
                     lambda: c.create_library(args.name, owner=args.owner))


def cmd_lib_transfer(c: SeafileClient, args) -> int:
    detail = {"repo_id": args.repo_id, "new_owner": args.to}
    return _do_write(args, f"transfer library {args.repo_id}", detail,
                     lambda: c.transfer_library(args.repo_id, args.to))


def cmd_lib_delete(c: SeafileClient, args) -> int:
    detail = {"repo_id": args.repo_id, "note": "IRREVERSIBLE - deletes the library + data"}
    return _do_write(args, f"DELETE library {args.repo_id}", detail,
                     lambda: c.delete_library(args.repo_id))


def _share_result_check(result) -> int:
    """Seafile's admin bulk-share returns HTTP 200 with success[]/failed[] arrays;
    surface any per-recipient failure instead of reporting a blanket [OK]."""
    if isinstance(result, dict):
        failed = result.get("failed") or []
        if failed:
            print("[WARNING] some recipients were NOT shared:")
            for f in failed:
                who = f.get("email") or f.get("group_id") or f if isinstance(f, dict) else f
                emsg = f.get("error_msg") or f.get("error") or "failed" if isinstance(f, dict) else "failed"
                print(f"    {who}: {emsg}")
            ok = result.get("success") or result.get("shared_to") or []
            print(f"[PARTIAL] shared to {len(ok)}, failed {len(failed)}")
            return 1
    return 0


def cmd_share(c: SeafileClient, args) -> int:
    stype = "group" if args.group else "user"
    detail = {"repo_id": args.repo_id, "share_to": args.to,
              "permission": args.perm, "type": stype}
    return _do_write(args, f"share library {args.repo_id}", detail,
                     lambda: c.share_library(args.repo_id, args.to,
                                             permission=args.perm, share_type=stype),
                     on_result=_share_result_check)


def cmd_unshare(c: SeafileClient, args) -> int:
    stype = "group" if args.group else "user"
    detail = {"repo_id": args.repo_id, "share_to": args.to, "type": stype}
    return _do_write(args, f"revoke share on {args.repo_id}", detail,
                     lambda: c.unshare_library(args.repo_id, args.to, share_type=stype))


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="seafile", description="Seafile Pro backup inventory + admin")
    p.add_argument("--url", help="override server base URL (default internal Docker endpoint)")
    sub = p.add_subparsers(dest="cmd", required=True)

    sub.add_parser("status")
    u = sub.add_parser("users"); u.add_argument("--inactive", action="store_true"); u.add_argument("--json", action="store_true")
    l = sub.add_parser("libraries"); l.add_argument("--json", action="store_true")
    us = sub.add_parser("usage"); us.add_argument("--json", action="store_true")
    d = sub.add_parser("devices"); d.add_argument("--user"); d.add_argument("--json", action="store_true")
    a = sub.add_parser("audit"); a.add_argument("--client"); a.add_argument("--rmm", action="store_true"); a.add_argument("--json", action="store_true")

    # --- write ops (all preview-by-default; require --confirm to apply) ---
    uc = sub.add_parser("user-create", help="create an account")
    uc.add_argument("email"); uc.add_argument("--password", required=True)
    uc.add_argument("--name"); uc.add_argument("--quota-gb", type=float, dest="quota_gb")
    uc.add_argument("--role"); uc.add_argument("--inactive", action="store_true")
    uc.add_argument("--confirm", action="store_true")

    ust = sub.add_parser("user-set", help="update an account (activate/quota/name/role/password)")
    ust.add_argument("email")
    ust.add_argument("--active", action="store_true"); ust.add_argument("--inactive", action="store_true")
    ust.add_argument("--quota-gb", type=float, dest="quota_gb"); ust.add_argument("--name")
    ust.add_argument("--role"); ust.add_argument("--password")
    ust.add_argument("--confirm", action="store_true")

    q = sub.add_parser("quota", help="set a user's storage quota (decimal GB)")
    q.add_argument("email"); q.add_argument("--gb", type=float, required=True)
    q.add_argument("--confirm", action="store_true")

    ud = sub.add_parser("user-delete", help="delete an account (irreversible)")
    ud.add_argument("email"); ud.add_argument("--confirm", action="store_true")

    lc = sub.add_parser("lib-create", help="create a library")
    lc.add_argument("name"); lc.add_argument("--owner", help="owner email (default: admin)")
    lc.add_argument("--confirm", action="store_true")

    lt = sub.add_parser("lib-transfer", help="transfer library ownership")
    lt.add_argument("repo_id"); lt.add_argument("--to", required=True, help="new owner email")
    lt.add_argument("--confirm", action="store_true")

    ld = sub.add_parser("lib-delete", help="delete a library (irreversible)")
    ld.add_argument("repo_id"); ld.add_argument("--confirm", action="store_true")

    sh = sub.add_parser("share", help="share a library to user(s)/group(s)")
    sh.add_argument("repo_id"); sh.add_argument("--to", nargs="+", required=True,
                                                help="recipient email(s), or group id(s) with --group")
    sh.add_argument("--perm", choices=["r", "rw"], default="rw")
    sh.add_argument("--group", action="store_true", help="share_to are group ids")
    sh.add_argument("--confirm", action="store_true")

    un = sub.add_parser("unshare", help="revoke a library share")
    un.add_argument("repo_id"); un.add_argument("--to", required=True,
                                                help="recipient email, or group id with --group")
    un.add_argument("--group", action="store_true"); un.add_argument("--confirm", action="store_true")
    return p


def main(argv=None) -> int:
    args = build_parser().parse_args(argv)
    c = SeafileClient(args.url)
    handlers = {"status": cmd_status, "users": cmd_users, "libraries": cmd_libraries,
                "usage": cmd_usage, "devices": cmd_devices, "audit": cmd_audit,
                "user-create": cmd_user_create, "user-set": cmd_user_set,
                "quota": cmd_quota, "user-delete": cmd_user_delete,
                "lib-create": cmd_lib_create, "lib-transfer": cmd_lib_transfer,
                "lib-delete": cmd_lib_delete, "share": cmd_share, "unshare": cmd_unshare}
    try:
        return handlers[args.cmd](c, args)
    except bc.BackupError as exc:
        print(f"[ERROR] {exc}", file=sys.stderr)
        # best-effort error logging per house rule
        try:
            root = bc.resolve_root()
            bc.subprocess.run(["bash", str(root / ".claude/scripts/log-skill-error.sh"),
                               "seafile", str(exc)[:200]], timeout=15)
        except Exception:
            pass
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
