import ipaddress, os, re, shutil, subprocess
from pathlib import Path

STATE=Path('/var/lib/scpanel/firewall'); STATE.mkdir(parents=True, exist_ok=True)
SAFE_PROTO={'tcp','udp','tcp/udp'}
RESERVED_NAMES={'admin-http','admin-https','user-http','user-https'}

class FirewallError(Exception): pass

def _run(argv, timeout=30):
    return subprocess.run(argv, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout, check=False)

def backend():
    if shutil.which('ufw'): return 'ufw'
    if shutil.which('firewall-cmd'): return 'firewalld'
    return 'none'

def validate_port(v):
    try: p=int(v)
    except Exception: raise FirewallError('Invalid port.')
    if not 1 <= p <= 65535: raise FirewallError('Port must be between 1 and 65535.')
    return p

def validate_source(src):
    src=(src or '0.0.0.0/0').strip()
    if src in ('*','any','ANY'): return '0.0.0.0/0'
    try:
        net=ipaddress.ip_network(src, strict=False)
    except ValueError as e: raise FirewallError('Source must be a valid IP/CIDR.') from e
    return str(net)

def validate_proto(proto):
    proto=(proto or 'tcp').lower().strip()
    if proto not in SAFE_PROTO: raise FirewallError('Protocol must be TCP, UDP or TCP/UDP.')
    return proto

def default_panel_ports(settings):
    return [
      ('admin-http', int(settings.get('admin_http_port','2086')), 'tcp'),
      ('admin-https', int(settings.get('admin_https_port','2087')), 'tcp'),
      ('user-http', int(settings.get('user_http_port','2082')), 'tcp'),
      ('user-https', int(settings.get('user_https_port','2083')), 'tcp'),
    ]

def _cmd_for(rule, action):
    b=backend(); p=str(rule['port']); proto=rule['protocol']; src=rule['source']
    if b=='ufw':
        protos=['tcp','udp'] if proto=='tcp/udp' else [proto]
        return [['ufw', action, f'{p}/{pr}'] if src=='0.0.0.0/0' else ['ufw', action, 'from', src, 'to', 'any', 'port', p, 'proto', pr] for pr in protos]
    if b=='firewalld':
        # firewalld rich rules provide source CIDR restrictions and exact protocol/port.
        protos=['tcp','udp'] if proto=='tcp/udp' else [proto]
        out=[]
        for pr in protos:
            if src=='0.0.0.0/0': out.append(['firewall-cmd','--permanent',f'--{action}-port={p}/{pr}'])
            else: out.append(['firewall-cmd','--permanent',f'--{action}-rich-rule',f'rule family="ipv4" source address="{src}" port port="{p}" protocol="{pr}" accept'])
        return out
    raise FirewallError('No supported firewall backend is installed. Install UFW or firewalld from App Store.')

def apply(rule, enabled=True):
    b=backend()
    if b=='none': raise FirewallError('No supported firewall backend is installed.')
    commands=_cmd_for(rule,'allow' if enabled else 'delete')
    for cmd in commands:
        r=_run(cmd)
        if r.returncode and enabled: raise FirewallError(r.stdout.strip()[-600:] or 'Firewall rule failed.')
    if b=='ufw' and enabled:
        # Do not silently enable UFW; service state is controlled by the administrator.
        pass
    if b=='firewalld':
        r=_run(['firewall-cmd','--reload'])
        if r.returncode: raise FirewallError(r.stdout.strip()[-600:] or 'firewalld reload failed.')

def systemd_panel_ports(settings):
    found=[]
    for unit in ('scpanel-admin.service','scpanel-user.service','scpanel-admin-http.service'):
        p=Path('/etc/systemd/system')/unit
        if p.exists():
            txt=p.read_text(errors='replace')
            found += [int(x) for x in re.findall(r'(?:--bind|:)(?:0\.0\.0\.0|127\.0\.0\.1)?:(\d{2,5})\b',txt)]
    return sorted(set(found))

def sync_panel_ports(settings):
    expected={p for _,p,_ in default_panel_ports(settings)}
    actual=set(systemd_panel_ports(settings))
    # 2082 may be legacy/unused; 2086 is redirect service and is protected too.
    expected.update({2086,2087,2083})
    missing=expected-actual
    # We intentionally don't rewrite systemd units here: firewall synchronization must
    # never change listener configuration implicitly. A port is protected if configured
    # in SCpanel or found in its systemd units.
    return {'expected':sorted(expected),'systemd_listeners':sorted(actual),'missing_listeners':sorted(missing)}

def protect_port(port, settings):
    p=int(port); return p in {x[1] for x in default_panel_ports(settings)} or p in {2086,2087,2083}

def status():
    b=backend(); active='unknown'
    if b=='ufw': active=_run(['ufw','status']).stdout.strip()
    elif b=='firewalld': active=_run(['firewall-cmd','--state']).stdout.strip()
    return {'backend':b,'active':active}
