#!/usr/bin/env python3
"""Fetch a licensed Senda snapshot; update atomically and preserve data on failures."""
import argparse
import ipaddress
import json
import os
from pathlib import Path
import subprocess
import tempfile
import urllib.error
import urllib.parse
import urllib.request


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        return None


def validate_list(data):
    text=data.decode("utf-8")
    ips=set()
    for line in text.splitlines():
        value=line.strip()
        if not value: continue
        ip=ipaddress.ip_address(value)
        if not ip.is_global or ip.is_multicast or ip.is_reserved: raise ValueError("Non-public IP in feed")
        ips.add(ip)
    return "".join(str(ip)+"\n" for ip in sorted(ips,key=lambda x:(x.version,int(x))))


def atomic_write(path, data):
    path=Path(path)
    path.parent.mkdir(parents=True,exist_ok=True)
    fd,name=tempfile.mkstemp(dir=path.parent,prefix=".senda-")
    try:
        with os.fdopen(fd,"wb") as out:
            out.write(data)
            out.flush()
            os.fsync(out.fileno())
        os.chmod(name,0o644)
        os.replace(name,path)
    finally:
        if os.path.exists(name): os.unlink(name)


def run(config, apply_only=False):
    cfg=json.loads(Path(config).read_text())
    category=int(cfg.get("category_id",1))
    if not 1<=category<=60: raise ValueError("category_id must be 1..60")
    directory=Path(cfg.get("directory","/etc/suricata/iprep"))
    downloaded=directory/"senda.download.txt"
    destination=directory/"senda.reputation"
    if apply_only:
        text=validate_list(downloaded.read_bytes())
    else:
        url=cfg["url"]
        parsed=urllib.parse.urlsplit(url)
        if parsed.scheme not in {"https","http"} or not parsed.netloc or parsed.username or parsed.password:
            raise ValueError("Invalid feed URL")
        key=cfg["api_key"]
        req=urllib.request.Request(url,headers={"Authorization":"Bearer "+key,"Accept":"text/plain"})
        with urllib.request.build_opener(NoRedirect()).open(req,timeout=90) as response:
            if response.headers.get_content_type()!="text/plain": raise ValueError("Unexpected feed format")
            data=response.read(8*1024*1024+1)
            if len(data)>8*1024*1024: raise ValueError("Feed too large")
        text=validate_list(data)
        atomic_write(downloaded,text.encode())
    reputation="".join(f"{ip},{category},100\n" for ip in text.splitlines()).encode()
    previous=destination.read_bytes() if destination.exists() else None
    atomic_write(destination,reputation)
    try:
        # USR2 reloads reputation files. Initial categories/config changes require restart.
        service=cfg.get("service","suricata")
        subprocess.run(["systemctl","kill","--kill-who=main","--signal=USR2",service],check=True)
    except Exception:
        if previous is not None: atomic_write(destination,previous)
        else: destination.unlink(missing_ok=True)
        raise
    print(f"Senda: installed {len(text.splitlines())} IPs")


def main():
    parser=argparse.ArgumentParser()
    parser.add_argument("--config",default="/etc/suricata/senda-feed.json")
    parser.add_argument("--apply-only",action="store_true")
    args=parser.parse_args()
    try:
        run(args.config,args.apply_only)
    except urllib.error.HTTPError as exc:
        if exc.code==429:
            retry=exc.headers.get("Retry-After","")
            print("Senda: download limit; existing file retained. Retry-After:",retry)
            return 0
        print("Senda: HTTP error",exc.code,"; existing file retained")
        return 1
    except Exception:
        print("Senda: update failed; applied file retained. Retry local installation with --apply-only if downloaded.")
        return 1
    return 0


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