"""Only the explicitly adopted Redis chain; no global rules or firewall service changes."""
import ipaddress
import json
from pathlib import Path
import shlex
import subprocess

import server_context as server


def ip(value):
    value=ipaddress.IPv4Address(value)
    if value.is_multicast or value.is_unspecified:raise ValueError('Invalid Redis address')
    return str(value)


def run(*args,check=True):
    return subprocess.run(['iptables','-w','5',*args],check=check,capture_output=True,text=True,timeout=15)


def owned_rules(lines,chain):
    for line in lines:
        p=shlex.split(line)
        if p==['-N',chain]:continue
        if p in (['-A',chain,'-i','lo','-j','RETURN'],['-A',chain,'-j','DROP']):continue
        if len(p)==6 and p[:3]==['-A',chain,'-s'] and p[4:]==['-j','RETURN']:
            n=ipaddress.IPv4Network(p[3],strict=False)
            if n.prefixlen==32:continue
        raise ValueError('Unexpected Redis chain rule; no mutation')


def apply():
    f=server.CURRENT['firewall'];chain=f['chain']
    p=server.safe(Path(f['script']).with_name('access.json'))
    d=json.loads(p.read_text());host=ip(d['host']);peers=sorted(set(ip(x) for x in d['readers']))
    if type(d['port']) is not int or not 1024<=d['port']<=65535 or not 1<=len(peers)<=32:raise ValueError('Invalid Redis policy')
    current=run('-S',chain,check=False)
    if current.returncode==0:owned_rules(current.stdout.splitlines(),chain)
    jumps=[]
    for parent in ('INPUT','DOCKER-USER'):
        listing=run('-S',parent,check=False)
        if listing.returncode and parent=='DOCKER-USER':continue
        if listing.returncode:raise ValueError('Cannot inspect firewall')
        for line in listing.stdout.splitlines():
            parts=shlex.split(line)
            if '-j' in parts and parts[parts.index('-j')+1]==chain:
                if parts[:2]!=['-A',parent] or '-p' not in parts or parts[parts.index('-p')+1]!='tcp':
                    raise ValueError('Unexpected reference to Redis chain')
                jumps.append(parts)
    # All ownership checks precede writes. Only this chain and its references are changed.
    if current.returncode:run('-N',chain)
    for parts in jumps:run('-D',*parts[1:])
    run('-F',chain)
    run('-A',chain,'-i','lo','-j','RETURN')
    for peer in peers:run('-A',chain,'-s',peer+'/32','-j','RETURN')
    run('-A',chain,'-j','DROP')
    if run('-S','DOCKER-USER',check=False).returncode:run('-N','DOCKER-USER')
    run('-I','INPUT','1','-d',host+'/32','-p','tcp','--dport',str(d['port']),'-j',chain)
    run('-I','DOCKER-USER','1','-p','tcp','-m','conntrack','--ctdir','ORIGINAL','--ctorigdst',host,
        '--ctorigdstport',str(d['port']),'-j',chain)


if __name__=='__main__':apply()
