"""Protect a distinct edge vhost while retaining an app's released direct-host address."""
from pathlib import Path
import re
import stat
import subprocess
import sys

from configure_vhost import content, replace
from edge_inputs import load

NGINX_DIR = Path("/etc/nginx/conf.d")


def paths(config, app, nginx_dir=NGINX_DIR):
    if not re.fullmatch(r"[a-z0-9-]+", app):
        raise ValueError("backend_app_invalid")
    return (nginx_dir / f"backend-{app}.conf", nginx_dir / f"backend-{app}-edge.conf",
            Path(config).parent / ".backend-ingress.conf")


def previous(path):
    if path.is_symlink():
        raise ValueError("backend_vhost_symlink")
    return path.read_text() if path.exists() else ""


def recorded_domain(path):
    text = previous(path)
    if not text:
        return ""
    names = set(re.findall(r"(?m)^\s*server_name\s+([^;]+);", text))
    if len(names) != 1:
        raise ValueError("backend_vhost_identity_invalid")
    domain = names.pop().strip()
    content(text, domain, 1)
    return domain


def validate(config, app, domain, nginx_dir=NGINX_DIR):
    value = load(config)
    legacy, edge, include = paths(config, app, nginx_dir)
    protected = bool(previous(edge) or previous(include))
    if value["ingress_required"] != "true":
        if protected:
            raise ValueError("backend_ingress_downgrade_refused")
        return value
    content("", domain, 1)
    old_domain = recorded_domain(legacy)
    if old_domain == domain:
        raise ValueError("backend_edge_domain_must_be_distinct")
    if previous(edge) and recorded_domain(edge) != domain:
        raise ValueError("backend_edge_domain_changed")
    return value


def edge_content(text, domain, port, include):
    updated = content(text, domain, port)
    directive = f"        include {include};\n"
    if re.search(r"[\s;{}]", str(include)):
        raise ValueError("backend_ingress_path_invalid")
    if updated.count(directive) == 1:
        return updated
    if ".backend-ingress.conf" in updated or len(re.findall(r"\bproxy_pass\b", updated)) != 1:
        raise ValueError("backend_edge_vhost_invalid")
    pattern = r"(?m)^(\s*location / \{\n)"
    updated, count = re.subn(pattern, lambda match: match[1] + directive, updated)
    if count != 1:
        raise ValueError("backend_edge_location_invalid")
    return updated


def planned(config, app, domain, port, nginx_dir=NGINX_DIR):
    value = validate(config, app, domain, nginx_dir)
    if value["ingress_required"] != "true":
        raise ValueError("backend_ingress_configuration_required")
    legacy, edge, include = paths(config, app, nginx_dir)
    token = value["ingress_token"]
    guard = (f'if ($http_x_app_robot_backend_token != "{token}") {{ return 403; }}\n'
             'proxy_set_header X-App-Robot-Backend-Token "";\n')
    files = {include: (guard.encode(), 0o600),
             edge: (edge_content(previous(edge), domain, port, include).encode(), 0o644)}
    old_domain = recorded_domain(legacy)
    if old_domain:
        files[legacy] = (content(previous(legacy), old_domain, port).encode(),
                         stat.S_IMODE(legacy.stat().st_mode))
    return files


def configure(config, app, domain, port, nginx_dir=NGINX_DIR, run=subprocess.run):
    files = planned(config, app, domain, port, nginx_dir)
    saved = {path: ((path.read_bytes(), stat.S_IMODE(path.stat().st_mode)) if path.exists() else None)
             for path in files}
    changes = {path: value for path, value in files.items() if saved[path] != value}
    if not changes:
        return "unchanged"
    try:
        for path, (data, mode) in changes.items():
            replace(path, data, mode)
        run(["nginx", "-t"], capture_output=True, timeout=30, check=True)
        run(["systemctl", "reload", "nginx"], capture_output=True, timeout=30, check=True)
    except (OSError, subprocess.SubprocessError):
        for path in changes:
            if saved[path] is None:
                path.unlink(missing_ok=True)
            else:
                replace(path, *saved[path])
        raise ValueError("backend_edge_nginx_failed") from None
    return "changed"


def main():
    operation, config, app, domain = sys.argv[1:5]
    if operation == "validate":
        validate(config, app, domain)
    elif operation == "configure":
        print(configure(config, app, domain, int(sys.argv[5])))
    else:
        raise ValueError("backend_edge_operation_invalid")


if __name__ == "__main__":
    try:
        main()
    except (OSError, ValueError):
        sys.exit("backend_edge_vhost_refused")
