#!/usr/bin/env python3
"""Privileged helper for linux-share-mounter. Invoked via pkexec as root.

Reads a JSON payload from stdin (when required by the action) and writes a
single JSON result line to stdout. Never invoke this directly as a normal
user for anything but 'list' - it manages /etc/fstab, credential files and
mounts system-wide.
"""
import glob
import json
import os
import pwd
import subprocess
import sys

DB_DIR = "/etc/linux-share-mounter"
DB_FILE = os.path.join(DB_DIR, "shares.json")
CREDS_DIR = "/etc/samba/credentials"
FSTAB_FILE = "/etc/fstab"
MOUNT_BASE = "/mnt/sharemounter"
BLOCK_BEGIN = "# BEGIN linux-share-mounter (managed - do not edit manually)"
BLOCK_END = "# END linux-share-mounter"


def fail(msg, code=1):
    print(json.dumps({"ok": False, "error": msg}))
    sys.exit(code)


def load_db():
    if not os.path.exists(DB_FILE):
        return {"shares": {}}
    with open(DB_FILE, "r") as f:
        try:
            return json.load(f)
        except json.JSONDecodeError:
            return {"shares": {}}


def save_db(db):
    os.makedirs(DB_DIR, exist_ok=True)
    tmp = DB_FILE + ".tmp"
    with open(tmp, "w") as f:
        json.dump(db, f, indent=2)
    os.chmod(tmp, 0o600)
    os.replace(tmp, DB_FILE)
    os.chmod(DB_DIR, 0o700)


def creds_path(name):
    return os.path.join(CREDS_DIR, name)


def write_credentials(name, entry):
    if not entry.get("username"):
        # guest share, no credentials file needed
        path = creds_path(name)
        if os.path.exists(path):
            os.remove(path)
        return None
    os.makedirs(CREDS_DIR, exist_ok=True)
    os.chmod(CREDS_DIR, 0o700)
    path = creds_path(name)
    lines = [
        f"username={entry.get('username', '')}",
        f"password={entry.get('password', '')}",
    ]
    if entry.get("domain"):
        lines.append(f"domain={entry['domain']}")
    tmp = path + ".tmp"
    with open(tmp, "w") as f:
        f.write("\n".join(lines) + "\n")
    os.chmod(tmp, 0o600)
    os.replace(tmp, path)
    return path


def build_fstab_line(name, entry):
    server = entry["server"].strip().lstrip("/\\")
    share = entry["share"].strip().lstrip("/\\")
    unc = f"//{server}/{share}"
    mountpoint = entry["mountpoint"]

    opts = [
        "_netdev",
        "x-systemd.automount",
        "x-systemd.mount-timeout=30",
        f"uid={entry.get('uid', 0)}",
        f"gid={entry.get('gid', 0)}",
        "file_mode=0664",
        "dir_mode=0775",
    ]
    if entry.get("username"):
        opts.insert(0, f"credentials={creds_path(name)}")
    else:
        opts.insert(0, "guest")
    if entry.get("smb_version"):
        opts.append(f"vers={entry['smb_version']}")
    if entry.get("extra_options"):
        opts.append(entry["extra_options"])

    return f"{unc}\t{mountpoint}\tcifs\t{','.join(opts)}\t0\t0"


def regenerate_fstab(db):
    if os.path.exists(FSTAB_FILE):
        with open(FSTAB_FILE, "r") as f:
            lines = f.read().splitlines()
    else:
        lines = []

    try:
        start = lines.index(BLOCK_BEGIN)
        end = lines.index(BLOCK_END)
        before = lines[:start]
        after = lines[end + 1:]
    except ValueError:
        before = lines
        after = []

    block = [BLOCK_BEGIN]
    for name, entry in sorted(db["shares"].items()):
        if entry.get("enabled"):
            block.append(f"# name: {name}")
            block.append(build_fstab_line(name, entry))
    block.append(BLOCK_END)

    new_lines = before + block + after
    tmp = FSTAB_FILE + ".tmp"
    with open(tmp, "w") as f:
        f.write("\n".join(new_lines) + "\n")
    os.chmod(tmp, 0o644)
    os.replace(tmp, FSTAB_FILE)

    subprocess.run(["systemctl", "daemon-reload"], check=False)


def ensure_mountpoint(path):
    os.makedirs(path, exist_ok=True)
    os.chmod(path, 0o755)


def do_mount(mountpoint):
    r = subprocess.run(["mount", mountpoint], capture_output=True, text=True)
    return r.returncode == 0, (r.stderr or r.stdout).strip()


def do_umount(mountpoint):
    r = subprocess.run(["umount", mountpoint], capture_output=True, text=True)
    return r.returncode == 0, (r.stderr or r.stdout).strip()


def get_caller_uid_gid():
    uid_str = os.environ.get("PKEXEC_UID")
    if uid_str is None:
        return 0, 0
    uid = int(uid_str)
    try:
        pw = pwd.getpwuid(uid)
        return pw.pw_uid, pw.pw_gid
    except KeyError:
        return uid, uid


def read_payload():
    data = sys.stdin.read()
    if not data.strip():
        return {}
    return json.loads(data)


def action_list():
    db = load_db()
    out = []
    for name, e in sorted(db["shares"].items()):
        out.append({
            "name": name,
            "server": e.get("server", ""),
            "share": e.get("share", ""),
            "mountpoint": e.get("mountpoint", ""),
            "username": e.get("username", ""),
            "domain": e.get("domain", ""),
            "smb_version": e.get("smb_version", ""),
            "extra_options": e.get("extra_options", ""),
            "enabled": e.get("enabled", False),
            "has_password": bool(e.get("password")),
        })
    print(json.dumps({"ok": True, "shares": out}))


def action_save(payload):
    name = payload.get("name", "").strip()
    if not name:
        fail("Name darf nicht leer sein")
    if any(c in name for c in "/\\ \t\n"):
        fail("Name darf keine Leerzeichen oder Schrägstriche enthalten")

    db = load_db()
    existing = db["shares"].get(name, {})

    password = payload.get("password", None)
    if password is None:
        password = existing.get("password", "")

    mountpoint = payload.get("mountpoint") or os.path.join(MOUNT_BASE, name)
    uid, gid = get_caller_uid_gid()

    entry = {
        "server": payload["server"].strip(),
        "share": payload["share"].strip(),
        "mountpoint": mountpoint,
        "username": payload.get("username", "").strip(),
        "password": password,
        "domain": payload.get("domain", "").strip(),
        "smb_version": payload.get("smb_version", "").strip(),
        "extra_options": payload.get("extra_options", "").strip(),
        "enabled": bool(payload.get("enabled", True)),
        "uid": existing.get("uid", uid),
        "gid": existing.get("gid", gid),
    }

    ensure_mountpoint(mountpoint)
    write_credentials(name, entry)

    db["shares"][name] = entry
    save_db(db)
    regenerate_fstab(db)

    mounted = False
    error = None
    if entry["enabled"]:
        ok, msg = do_mount(mountpoint)
        mounted = ok
        if not ok and "already mounted" not in msg.lower():
            error = msg

    print(json.dumps({"ok": True, "mounted": mounted, "error": error, "mountpoint": mountpoint}))


def action_remove(payload):
    name = payload.get("name", "").strip()
    db = load_db()
    entry = db["shares"].get(name)
    if not entry:
        fail(f"Freigabe '{name}' nicht gefunden")

    do_umount(entry["mountpoint"])

    path = creds_path(name)
    if os.path.exists(path):
        os.remove(path)

    try:
        os.rmdir(entry["mountpoint"])
    except OSError:
        pass

    del db["shares"][name]
    save_db(db)
    regenerate_fstab(db)
    print(json.dumps({"ok": True}))


def action_set_enabled(payload):
    name = payload.get("name", "").strip()
    enabled = bool(payload.get("enabled"))
    db = load_db()
    entry = db["shares"].get(name)
    if not entry:
        fail(f"Freigabe '{name}' nicht gefunden")

    entry["enabled"] = enabled
    save_db(db)
    regenerate_fstab(db)

    if enabled:
        ensure_mountpoint(entry["mountpoint"])
        ok, msg = do_mount(entry["mountpoint"])
        print(json.dumps({"ok": True, "mounted": ok, "error": None if ok else msg}))
    else:
        ok, msg = do_umount(entry["mountpoint"])
        print(json.dumps({"ok": True, "unmounted": ok, "error": None if ok else msg}))


def action_mount(payload):
    name = payload.get("name", "").strip()
    db = load_db()
    entry = db["shares"].get(name)
    if not entry:
        fail(f"Freigabe '{name}' nicht gefunden")
    ensure_mountpoint(entry["mountpoint"])
    ok, msg = do_mount(entry["mountpoint"])
    print(json.dumps({"ok": ok, "error": None if ok else msg}))


def action_unmount(payload):
    name = payload.get("name", "").strip()
    db = load_db()
    entry = db["shares"].get(name)
    if not entry:
        fail(f"Freigabe '{name}' nicht gefunden")
    ok, msg = do_umount(entry["mountpoint"])
    print(json.dumps({"ok": ok, "error": None if ok else msg}))


ACTIONS = {
    "list": lambda: action_list(),
    "save": lambda: action_save(read_payload()),
    "remove": lambda: action_remove(read_payload()),
    "set-enabled": lambda: action_set_enabled(read_payload()),
    "mount": lambda: action_mount(read_payload()),
    "unmount": lambda: action_unmount(read_payload()),
}


def main():
    if len(sys.argv) != 2 or sys.argv[1] not in ACTIONS:
        fail(f"Usage: {sys.argv[0]} <{'|'.join(ACTIONS)}>")
    if os.geteuid() != 0:
        fail("Muss als root laufen (via pkexec)")
    try:
        ACTIONS[sys.argv[1]]()
    except Exception as e:
        fail(str(e))


if __name__ == "__main__":
    main()
