#!/data/data/com.termux/files/usr/bin/python3
"""mdns: unified CLI for mDNS resolve / browse / publish / reverse / cache.

Loads lib/mdns_tools/ from the repo root. Usable both from an install into
~/.local/bin (installer sets a symlink pointing at the repo checkout) and
directly from a clone.
"""
import argparse
import json
import os
import sys
from pathlib import Path

_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_ROOT / "lib"))

from mdns_tools import __version__  # noqa: E402


def _cmd_resolve(a: argparse.Namespace) -> int:
    from mdns_tools import resolve as _r, cache as _c
    ip = _r.resolve(a.host, family=a.family, timeout=a.timeout,
                    retries=a.retries, use_cache=not a.no_cache)
    if ip:
        print(ip)
        return 0
    if a.stale:
        ip = _c.get_stale(a.host if a.host.endswith(".local") else f"{a.host}.local",
                          family=a.family)
        if ip:
            print(ip)
            return 0
    print(f"error: could not resolve '{a.host}'", file=sys.stderr)
    return 1


def _cmd_reverse(a: argparse.Namespace) -> int:
    from mdns_tools import reverse as _rev
    name = _rev.reverse(a.ip, timeout=a.timeout, retries=a.retries)
    if name:
        print(name)
        return 0
    print(f"error: no PTR for {a.ip}", file=sys.stderr)
    return 1


def _cmd_browse(a: argparse.Namespace) -> int:
    if a.watch:
        from mdns_tools import watch as _w
        _w.watch(service_type=a.type, resolve=not a.no_resolve)
        return 0
    from mdns_tools import browse as _br  # loads zeroconf
    if a.list_types:
        for t in _br.list_types(duration=a.duration):
            print(t)
        return 0
    items = _br.browse(service_type=a.type, duration=a.duration)
    if a.json:
        print(json.dumps(items, indent=2))
        return 0
    for it in items:
        addrs = ", ".join(it.get("addresses") or []) or "-"
        print(f'{it["type"]:<30} {it.get("server","-"):<28} {addrs}:{it.get("port","-")}')
    return 0


def _cmd_publish(a: argparse.Namespace) -> int:
    from mdns_tools import publish as _p  # loads zeroconf
    if a.address:
        _p.publish_address(a.address[0], a.address[1], verbose=not a.quiet)
        return 0
    if a.daemon:
        from mdns_tools import service as _s
        return _s.run_daemon(config_path=a.config, hostname=a.hostname,
                             verbose=not a.quiet)
    _p.run(config_path=a.config, hostname=a.hostname, verbose=not a.quiet)
    return 0


def _cmd_status(a: argparse.Namespace) -> int:
    from mdns_tools import service as _s
    return _s.status()


def _cmd_stop(a: argparse.Namespace) -> int:
    from mdns_tools import service as _s
    return _s.stop()


def _cmd_sync_hosts(a: argparse.Namespace) -> int:
    from mdns_tools import hosts as _h
    return _h.sync(duration=a.duration, dry_run=a.dry_run)


def _cmd_cache(a: argparse.Namespace) -> int:
    from mdns_tools import cache as _c
    if a.op == "list":
        for k, v in _c.all_entries().items():
            host, fam = k.split("|") if "|" in k else (k, "v4")
            print(f'{host:<40} {fam}  {v.get("ip"):<40} ttl={v.get("ttl")}')
    elif a.op == "clear":
        n = _c.clear(a.host)
        print(f"removed {n} entries")
    return 0


def main() -> int:
    p = argparse.ArgumentParser(prog="mdns", description="mDNS / DNS-SD tools")
    p.add_argument("--version", action="version", version=f"mdns {__version__}")
    sub = p.add_subparsers(dest="cmd", required=True)

    r = sub.add_parser("resolve", help="hostname -> IP")
    r.add_argument("host")
    r.add_argument("--family", choices=["v4", "v6"], default="v4")
    r.add_argument("--timeout", type=float, default=1.5)
    r.add_argument("--retries", type=int, default=2)
    r.add_argument("--no-cache", action="store_true")
    r.add_argument("--stale", action="store_true", help="accept expired cache as last resort")
    r.set_defaults(func=_cmd_resolve)

    rv = sub.add_parser("reverse", help="IP -> hostname (PTR)")
    rv.add_argument("ip")
    rv.add_argument("--timeout", type=float, default=1.5)
    rv.add_argument("--retries", type=int, default=2)
    rv.set_defaults(func=_cmd_reverse)

    b = sub.add_parser("browse", help="discover services on the LAN")
    b.add_argument("--type", help="e.g. _ssh._tcp.local. (default: all types)")
    b.add_argument("--duration", type=float, default=3.0)
    b.add_argument("--json", action="store_true")
    b.add_argument("--list-types", action="store_true")
    b.add_argument("--watch", action="store_true",
                   help="live monitor (+/=/- events) until Ctrl+C")
    b.add_argument("--no-resolve", action="store_true",
                   help="in --watch, don't resolve addresses")
    b.set_defaults(func=_cmd_browse)

    pu = sub.add_parser("publish", help="advertise this device + services")
    pu.add_argument("--config", help="path to services JSON")
    pu.add_argument("--hostname")
    pu.add_argument("--address", nargs=2, metavar=("NAME", "IP"),
                    help="publish a bare hostname->IP (A record), like avahi-publish -a")
    pu.add_argument("--daemon", action="store_true",
                    help="run as singleton daemon: re-announce on IP change, clean goodbye")
    pu.add_argument("--quiet", action="store_true")
    pu.set_defaults(func=_cmd_publish)

    sub.add_parser("status", help="show publisher daemon state").set_defaults(func=_cmd_status)
    sub.add_parser("stop", help="stop the publisher daemon").set_defaults(func=_cmd_stop)

    sh = sub.add_parser("sync-hosts", help="write discovered hosts into the hosts file")
    sh.add_argument("--duration", type=float, default=3.0)
    sh.add_argument("--dry-run", action="store_true")
    sh.set_defaults(func=_cmd_sync_hosts)

    ca = sub.add_parser("cache", help="inspect/clear the resolver cache")
    ca.add_argument("op", choices=["list", "clear"])
    ca.add_argument("host", nargs="?")
    ca.set_defaults(func=_cmd_cache)

    args = p.parse_args()
    return args.func(args)


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