#!/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 jd(obj: Any) -> str:
    return json.dumps(obj, separators=(",", ":"), ensure_ascii=False)


def emit(name: str, payload: Any) -> None:
    print(f"<<<{name}:sep(0)>>>")
    print(jd(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, tenant: str, admin_url: str) -> Path:
    omd = Path(os.environ.get("OMD_ROOT", "/tmp"))
    key = hashlib.sha256(f"{app_id}|{tenant}|{admin_url}".encode()).hexdigest()[:16]
    path = omd / "var" / "check_mk" / "microsoft_365_sharepoint_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(jd(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,
        },
        "tenant": None,
        "sites": None,
        "app_catalog": None,
        "themes": None,
        "details": {"sites": {"enabled": False, "entities": []}},
    }


def args_parser() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Checkmk Microsoft 365 SharePoint configuration special agent")
    p.add_argument("--app-id", required=True)
    p.add_argument("--tenant", required=True)
    p.add_argument("--admin-url", required=True)
    p.add_argument("--certificate-path", required=True)
    parser_add_secret_option(p, long="--pfx-password", help="Password for SharePoint certificate PFX", required=True)
    p.add_argument("--cache-seconds", type=int, default=1800)
    p.add_argument("--timeout", type=int, default=300)
    p.add_argument("--max-sites", type=int, default=100)
    p.add_argument("--enable-site-detail", action="store_true")
    p.add_argument("--site-filter", default=".*")
    p.add_argument("--include-onedrive-sites", action="store_true")
    return p.parse_args()


def collect(a: argparse.Namespace, password: str, certificate: Path) -> dict[str, Any]:
    script = Path(__file__).with_name("m365_sharepoint_config_collector.ps1")
    cmd = [
        "pwsh", "-NoLogo", "-NoProfile", "-NonInteractive", "-File", str(script),
        "-AppId", a.app_id,
        "-Tenant", a.tenant,
        "-AdminUrl", a.admin_url,
        "-CertificateFilePath", str(certificate),
        "-MaxSites", str(max(1, a.max_sites)),
        "-SiteFilter", a.site_filter,
    ]
    if a.enable_site_detail:
        cmd.append("-EnableSiteDetail")
    if a.include_onedrive_sites:
        cmd.append("-IncludeOneDriveSites")

    try:
        proc = subprocess.run(
            cmd,
            input=password + "\n",
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=max(30, a.timeout),
            check=False,
        )
    except subprocess.TimeoutExpired:
        return empty_payload(f"SharePoint configuration collection timed out after {a.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 SharePoint 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_spo_config_api", data.get("api") or {})
    emit("m365_spo_tenant", data.get("tenant") or {})
    emit("m365_spo_sites", data.get("sites") or {})
    emit("m365_spo_app_catalog", data.get("app_catalog") or {})
    emit("m365_spo_themes", data.get("themes") or {})
    emit("m365_spo_details", data.get("details") or {})


def main() -> int:
    a = args_parser()
    try:
        certificate = certificate_file(a.certificate_path)
    except ValueError as exc:
        output(empty_payload(str(exc)))
        return 0

    settings = {
        "max_sites": a.max_sites,
        "enable_site_detail": a.enable_site_detail,
        "site_filter": a.site_filter,
        "include_onedrive_sites": a.include_onedrive_sites,
    }
    cfg = hashlib.sha256(jd(settings).encode()).hexdigest()[:16]
    root = cache_root(a.app_id, a.tenant, a.admin_url)
    current = root / f"current_{cfg}.json"
    last_good = root / f"last_good_{cfg}.json"

    cached = read_json(current, max(0, a.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(a, "pfx_password").reveal()
    data = collect(a, 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

    lkg = read_json(last_good)
    if lkg 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 {})
        fallback = copy.deepcopy(lkg)
        fallback["api"] = failed_api
        fallback["api"]["ok"] = False
        fallback["api"]["from_cache"] = False
        fallback["api"]["using_last_good"] = True
        fallback["api"]["last_good_age_seconds"] = age
        fallback["api"]["version"] = VERSION
        output(fallback)
        return 0

    output(data)
    return 0


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