#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-only
# Copyright (C) 2026 Sudhir Chauhan

from __future__ import annotations

import argparse
import copy
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import time
from typing import Any

from cmk.password_store.v1_unstable import parser_add_secret_option, resolve_secret_option

VERSION = "0.0.1"
SENTINEL = "__CMK_JSON_BEGIN__"


def _json_dump(obj: Any) -> str:
    return json.dumps(obj, separators=(",", ":"), ensure_ascii=False)


def _emit(name: str, payload: Any) -> None:
    print(f"<<<{name}:sep(0)>>>")
    print(_json_dump(payload))


def _certificate_file(path_text: str) -> Path:
    expanded = os.path.expandvars(os.path.expanduser(path_text))
    raw_path = Path(expanded)
    if not raw_path.is_absolute():
        raise ValueError("Certificate path must be absolute")
    path = raw_path.resolve()
    if not path.is_file():
        raise ValueError(f"Certificate file does not exist or is not a regular file: {path}")
    if not os.access(path, os.R_OK):
        raise ValueError(f"Certificate file is not readable by the Checkmk site user: {path}")
    return path


def _cache_root(app_id: str, organization: str) -> Path:
    root = Path(os.environ.get("OMD_ROOT", "/tmp"))
    key = hashlib.sha256(f"{app_id}|{organization}".encode()).hexdigest()[:16]
    path = root / "var" / "check_mk" / "microsoft_365_exchange_config_cache" / key
    path.mkdir(parents=True, exist_ok=True)
    try:
        path.chmod(0o700)
    except OSError:
        pass
    return path


def _read_json(path: Path, max_age: int | None = None) -> dict[str, Any] | None:
    try:
        if max_age is not None and time.time() - path.stat().st_mtime > max_age:
            return None
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError, json.JSONDecodeError):
        return None


def _write_json(path: Path, data: dict[str, Any]) -> None:
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(_json_dump(data), encoding="utf-8")
    os.replace(tmp, path)
    try:
        path.chmod(0o600)
    except OSError:
        pass


def _empty_payload(error: str) -> dict[str, Any]:
    return {
        "api": {
            "ok": False,
            "error": error,
            "duration_seconds": 0.0,
            "from_cache": False,
            "using_last_good": False,
            "version": VERSION,
        },
        "transport": None,
        "organization": None,
        "owa_policies": [],
        "mobile_policies": [],
        "management_roles": None,
        "role_groups": None,
        "details": {
            "mailboxes": {"enabled": False, "entities": []},
            "users": {"enabled": False, "entities": []},
            "distribution_groups": {"enabled": False, "entities": []},
            "dynamic_groups": {"enabled": False, "entities": []},
            "mobile_devices": {"enabled": False, "entities": []},
        },
    }


def _parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Checkmk Microsoft 365 Exchange configuration special agent")
    parser.add_argument("--app-id", required=True)
    parser.add_argument("--organization", required=True)
    parser.add_argument("--certificate-path", required=True)
    parser_add_secret_option(
        parser,
        long="--pfx-password",
        help="Password for the Exchange Online certificate PFX",
        required=True,
    )
    parser.add_argument("--cache-seconds", type=int, default=1800)
    parser.add_argument("--timeout", type=int, default=180)
    parser.add_argument("--max-entities", type=int, default=100)
    parser.add_argument("--enable-mailbox-detail", action="store_true")
    parser.add_argument("--mailbox-filter", default=".*")
    parser.add_argument("--enable-user-detail", action="store_true")
    parser.add_argument("--user-filter", default=".*")
    parser.add_argument("--enable-distribution-group-detail", action="store_true")
    parser.add_argument("--distribution-group-filter", default=".*")
    parser.add_argument("--enable-dynamic-group-detail", action="store_true")
    parser.add_argument("--dynamic-group-filter", default=".*")
    parser.add_argument("--enable-mobile-device-detail", action="store_true")
    parser.add_argument("--mobile-device-filter", default=".*")
    return parser.parse_args()


def _collect(args: argparse.Namespace, password: str, certificate: Path) -> dict[str, Any]:
    script = Path(__file__).with_name("m365_exchange_config_collector.ps1")
    cmd = [
        "pwsh",
        "-NoLogo",
        "-NoProfile",
        "-NonInteractive",
        "-File",
        str(script),
        "-AppId",
        args.app_id,
        "-Organization",
        args.organization,
        "-CertificateFilePath",
        str(certificate),
        "-MaxEntities",
        str(max(1, args.max_entities)),
        "-MailboxFilter",
        args.mailbox_filter,
        "-UserFilter",
        args.user_filter,
        "-DistributionGroupFilter",
        args.distribution_group_filter,
        "-DynamicGroupFilter",
        args.dynamic_group_filter,
        "-MobileDeviceFilter",
        args.mobile_device_filter,
    ]
    for enabled, flag in (
        (args.enable_mailbox_detail, "-EnableMailboxDetail"),
        (args.enable_user_detail, "-EnableUserDetail"),
        (args.enable_distribution_group_detail, "-EnableDistributionGroupDetail"),
        (args.enable_dynamic_group_detail, "-EnableDynamicGroupDetail"),
        (args.enable_mobile_device_detail, "-EnableMobileDeviceDetail"),
    ):
        if enabled:
            cmd.append(flag)

    try:
        proc = subprocess.run(
            cmd,
            input=password + "\n",
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=max(30, args.timeout),
            check=False,
        )
    except subprocess.TimeoutExpired:
        return _empty_payload(f"Exchange Online configuration collection timed out after {args.timeout}s")
    except OSError as exc:
        return _empty_payload(f"Could not execute PowerShell: {exc}")

    pos = proc.stdout.find(SENTINEL)
    if pos < 0:
        detail = (proc.stderr or proc.stdout).strip()
        return _empty_payload(
            f"PowerShell collector produced no JSON payload (exit {proc.returncode}): {detail[:1200]}"
        )

    raw = proc.stdout[pos + len(SENTINEL) :].strip()
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as exc:
        return _empty_payload(f"Could not parse Exchange Online collector JSON: {exc}")

    data.setdefault("api", {})
    data["api"]["from_cache"] = False
    data["api"]["using_last_good"] = False
    data["api"]["version"] = VERSION
    if proc.returncode != 0 and not data["api"].get("error"):
        data["api"]["ok"] = False
        data["api"]["error"] = (proc.stderr or f"PowerShell exit code {proc.returncode}")[:1200]
    return data


def _output(data: dict[str, Any]) -> None:
    _emit("m365_exo_config_api", data.get("api") or {})
    _emit("m365_exo_transport", data.get("transport") or {})
    _emit("m365_exo_organization", data.get("organization") or {})
    _emit("m365_exo_owa_policies", {"policies": data.get("owa_policies", [])})
    _emit("m365_exo_mobile_policies", {"policies": data.get("mobile_policies", [])})
    _emit("m365_exo_management_roles", data.get("management_roles") or {})
    _emit("m365_exo_role_groups", data.get("role_groups") or {})
    _emit("m365_exo_details", data.get("details") or {})


def main() -> int:
    args = _parse_args()
    try:
        certificate = _certificate_file(args.certificate_path)
    except ValueError as exc:
        _output(_empty_payload(str(exc)))
        return 0

    settings = {
        "max_entities": args.max_entities,
        "mailbox": [args.enable_mailbox_detail, args.mailbox_filter],
        "user": [args.enable_user_detail, args.user_filter],
        "distribution_group": [args.enable_distribution_group_detail, args.distribution_group_filter],
        "dynamic_group": [args.enable_dynamic_group_detail, args.dynamic_group_filter],
        "mobile_device": [args.enable_mobile_device_detail, args.mobile_device_filter],
    }
    config_key = hashlib.sha256(_json_dump(settings).encode()).hexdigest()[:16]
    root = _cache_root(args.app_id, args.organization)
    current = root / f"current_{config_key}.json"
    last_good = root / f"last_good_{config_key}.json"

    cached = _read_json(current, max(0, args.cache_seconds))
    if cached is not None:
        cached.setdefault("api", {})
        cached["api"]["from_cache"] = True
        cached["api"]["using_last_good"] = False
        cached["api"]["version"] = VERSION
        _output(cached)
        return 0

    password = resolve_secret_option(args, "pfx_password").reveal()
    data = _collect(args, password, certificate)

    if data.get("api", {}).get("ok"):
        try:
            _write_json(current, data)
            _write_json(last_good, data)
        except OSError:
            pass
        _output(data)
        return 0

    fallback = _read_json(last_good)
    if fallback is not None:
        try:
            age = max(0, int(time.time() - last_good.stat().st_mtime))
        except OSError:
            age = 0
        failed_api = copy.deepcopy(data.get("api") or {})
        data = copy.deepcopy(fallback)
        data["api"] = failed_api
        data["api"]["ok"] = False
        data["api"]["from_cache"] = False
        data["api"]["using_last_good"] = True
        data["api"]["last_good_age_seconds"] = age
        data["api"]["version"] = VERSION

    _output(data)
    return 0


if __name__ == "__main__":
    sys.exit(main())
