#!/usr/bin/env python3

from __future__ import annotations

import argparse
import json
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from pathlib import Path

from cmk.utils import password_store


VERSION = "1.0.0"
API_COMMAND = "<show><vpn><flow/></vpn></show>"


def parse_arguments() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "Checkmk special agent for monitoring Palo Alto Networks "
            "IPSec tunnels through the PAN-OS XML API."
        )
    )

    parser.add_argument(
        "--host",
        required=True,
        help="Palo Alto Networks firewall management address",
    )

    parser.add_argument(
        "--api-key-store",
        required=True,
        help=(
            "Password store reference in the format "
            "<password-id>:<password-store-file>"
        ),
    )

    parser.add_argument(
        "--timeout",
        type=int,
        default=15,
        help="API request timeout in seconds",
    )

    parser.add_argument(
        "--verify-cert",
        action="store_true",
        help="Verify the HTTPS certificate presented by the firewall",
    )

    parser.add_argument(
        "--version",
        action="version",
        version=f"%(prog)s {VERSION}",
    )

    return parser.parse_args()


def resolve_api_key(password_store_reference: str) -> str:
    try:
        password_id, password_store_file = password_store_reference.split(":", 1)
    except ValueError as exc:
        raise ValueError(
            "invalid password store reference"
        ) from exc

    if not password_id or not password_store_file:
        raise ValueError("invalid password store reference")

    return password_store.lookup(
        Path(password_store_file),
        password_id,
    )


def create_ssl_context(verify_certificate: bool) -> ssl.SSLContext:
    if verify_certificate:
        return ssl.create_default_context()

    return ssl._create_unverified_context()


def create_request(host: str, api_key: str) -> urllib.request.Request:
    parameters = urllib.parse.urlencode(
        {
            "type": "op",
            "cmd": API_COMMAND,
        }
    )

    return urllib.request.Request(
        url=f"https://{host}/api/?{parameters}",
        headers={
            "X-PAN-KEY": api_key,
            "User-Agent": f"checkmk-paloalto-ipsec/{VERSION}",
        },
    )


def fetch_vpn_status(
    host: str,
    api_key: str,
    timeout: int,
    verify_certificate: bool,
) -> bytes:
    request = create_request(host, api_key)
    ssl_context = create_ssl_context(verify_certificate)

    with urllib.request.urlopen(
        request,
        context=ssl_context,
        timeout=timeout,
    ) as response:
        return response.read()


def get_api_error(root: ET.Element) -> str:
    messages = [
        text.strip()
        for text in root.itertext()
        if text and text.strip()
    ]

    if messages:
        return " ".join(messages)

    return "Unknown PAN-OS XML API error"


def parse_vpn_status(xml_data: bytes) -> list[dict[str, str]]:
    root = ET.fromstring(xml_data)

    if root.attrib.get("status") != "success":
        raise RuntimeError(get_api_error(root))

    tunnels: list[dict[str, str]] = []

    for entry in root.findall("./result/IPSec/entry"):
        tunnel = {
            "name": entry.findtext("name", "").strip(),
            "id": entry.findtext("id", "").strip(),
            "gateway_id": entry.findtext("gwid", "").strip(),
            "interface": entry.findtext("inner-if", "").strip(),
            "outer_interface": entry.findtext("outer-if", "").strip(),
            "local_ip": entry.findtext("localip", "").strip(),
            "peer_ip": entry.findtext("peerip", "").strip(),
            "state": entry.findtext("state", "unknown").strip().lower(),
            "monitor": entry.findtext("mon", "unknown").strip().lower(),
            "owner": entry.findtext("owner", "").strip(),
        }

        if not tunnel["name"]:
            continue

        tunnels.append(tunnel)

    return tunnels


def output_checkmk_section(tunnels: list[dict[str, str]]) -> None:
    print("<<<paloalto_ipsec:sep(0)>>>")
    print(
        json.dumps(
            tunnels,
            ensure_ascii=False,
            separators=(",", ":"),
            sort_keys=True,
        )
    )


def print_error(message: str) -> None:
    print(
        f"Special agent failed: {message}",
        file=sys.stderr,
    )


def main() -> int:
    args = parse_arguments()

    if args.timeout <= 0:
        print_error("timeout must be greater than zero")
        return 2

    try:
        api_key = resolve_api_key(args.api_key_store)

        xml_data = fetch_vpn_status(
            host=args.host,
            api_key=api_key,
            timeout=args.timeout,
            verify_certificate=args.verify_cert,
        )

        tunnels = parse_vpn_status(xml_data)
        output_checkmk_section(tunnels)

    except urllib.error.HTTPError as exc:
        print_error(f"HTTP {exc.code}: {exc.reason}")
        return 1

    except urllib.error.URLError as exc:
        print_error(f"connection failed: {exc.reason}")
        return 1

    except ssl.SSLError as exc:
        print_error(f"TLS error: {exc}")
        return 1

    except TimeoutError:
        print_error("API request timed out")
        return 1

    except ET.ParseError as exc:
        print_error(f"invalid XML response: {exc}")
        return 1

    except ValueError as exc:
        print_error(str(exc))
        return 1

    except RuntimeError as exc:
        print_error(str(exc))
        return 1

    except Exception as exc:
        print_error(f"unexpected error: {exc}")
        return 1

    return 0


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