#!/usr/bin/env python3
# -*- coding: utf-8; py-indent-offset: 4; max-line-length: 100 -*-

####################################################################################################
# CHECKMK SPECIAL AGENT: Microsoft Graph App Secrets
#
# Retrieves the expiration dates of Microsoft Entra app registration client secrets
# (passwordCredentials) using the Microsoft Graph API.
####################################################################################################

import argparse
import json
import sys
from pathlib import Path

import requests

import cmk.utils.password_store
from cmk.utils.http_proxy_config import deserialize_http_proxy_config, HTTPProxyConfig

GRAPH_API_BASE = "https://graph.microsoft.com"
GRAPH_API_V1 = f"{GRAPH_API_BASE}/v1.0"


def parse_arguments() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Checkmk special agent for Microsoft Graph app secrets",
    )
    parser.add_argument(
        "--tenant-id",
        required=True,
        help="Microsoft Entra tenant ID",
    )
    parser.add_argument(
        "--app-id",
        required=True,
        help="Application (client) ID of the Microsoft Entra app registration",
    )
    parser.add_argument(
        "--app-secret",
        required=True,
        help=(
            "Password store reference of the application (client) secret "
            "(e.g., 'secret_id:/omd/sites/<site>/var/check_mk/passwords_merged')"
        ),
    )
    parser.add_argument(
        "--proxy",
        required=False,
        help="HTTP proxy (FROM_ENVIRONMENT, NO_PROXY, or URL) (default: environment settings)",
    )
    parser.add_argument(
        "--timeout",
        required=False,
        type=float,
        default=10.0,
        help="API request timeout in seconds (default: %(default)s)",
    )

    return parser.parse_args()


def handle_error(err: Exception, context: str, exit_code: int = 1) -> None:
    err_msg = f"{err}"
    if hasattr(err, "response") and err.response is not None:
        err_msg += f" Response: {getattr(err.response, 'text', 'No response text')}"

    sys.stderr.write(f"{err_msg}\n\n{context}\n")

    sys.exit(exit_code)


def get_access_token(
    tenant_id: str,
    app_id: str,
    app_secret: str,
    timeout: float,
    proxy: HTTPProxyConfig,
) -> str:
    token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"

    headers = {"Content-Type": "application/x-www-form-urlencoded"}

    body = {
        "client_id": app_id,
        "client_secret": app_secret,
        "grant_type": "client_credentials",
        "scope": f"{GRAPH_API_BASE}/.default",
    }

    try:
        token_response = requests.post(
            token_url,
            headers=headers,
            data=body,
            timeout=timeout,
            proxies=proxy.to_requests_proxies(),
        )
        token_response.raise_for_status()
    except requests.exceptions.Timeout as err:
        handle_error(err, "Timeout while getting access token.", 10)
    except requests.exceptions.RequestException as err:
        error_message = "Failed to get access token."
        error_message_details = {
            400: f"{error_message} Please check tenant ID and client ID.",
            401: f"{error_message} Please check client secret.",
            429: f"{error_message} Request has been throttled.",
        }
        status_code = getattr(err.response, "status_code", 0)
        handle_error(err, error_message_details.get(status_code, error_message), 20)

    return token_response.json()["access_token"]


def get_app_registration_secrets(
    token: str, timeout: float, proxy: HTTPProxyConfig
) -> list[dict]:
    apps_url = (
        f"{GRAPH_API_V1}/applications"
        "?$select=appId,id,notes,displayName,passwordCredentials"
        "&$top=999"
    )

    headers = {"Authorization": f"Bearer {token}"}

    applications = []

    while apps_url:
        try:
            apps_response = requests.get(
                apps_url,
                headers=headers,
                timeout=timeout,
                proxies=proxy.to_requests_proxies(),
            )
            apps_response.raise_for_status()
        except requests.exceptions.Timeout as err:
            handle_error(err, "Timeout while getting Microsoft Entra app registrations.", 11)
        except requests.exceptions.RequestException as err:
            error_message = "Failed to get Microsoft Entra app registrations."
            error_message_details = {
                403: (
                    f"{error_message} Please check application API permissions. At least "
                    "Application.Read.All is required."
                ),
                429: f"{error_message} Request has been throttled.",
            }
            status_code = getattr(err.response, "status_code", 0)
            handle_error(err, error_message_details.get(status_code, error_message), 21)

        apps_dict = apps_response.json()
        applications.extend(apps_dict.get("value", []))

        # Get next page if available (pagination)
        apps_url = apps_dict.get("@odata.nextLink")

    app_names_seen: set[str] = set()
    app_list: list[dict] = []
    for app in applications:
        app_secrets = app.get("passwordCredentials", [])
        if not app_secrets:
            continue

        app_name = app["displayName"]
        app_id = app["id"]

        # Entra app registration names are not unique. Make the item name unique so that
        # Checkmk can tell services of same-named apps apart.
        if app_name in app_names_seen:
            app_name_unique = f"{app_name}_{app_id[-4:]}"
        else:
            app_name_unique = app_name
            app_names_seen.add(app_name)

        secrets_list = [
            {
                "secret_id": secret["keyId"],
                "secret_name": secret.get("displayName"),
                "secret_expiration": secret["endDateTime"],
            }
            for secret in app_secrets
        ]

        app_list.append(
            {
                "app_name": app_name_unique,
                "app_appid": app["appId"],
                "app_id": app_id,
                "app_notes": app.get("notes"),
                "secrets": secrets_list,
            }
        )

    return app_list


def main() -> None:
    args = parse_arguments()
    proxy = deserialize_http_proxy_config(args.proxy)

    pw_id, pw_path = args.app_secret.split(":")
    app_secret = cmk.utils.password_store.lookup(Path(pw_path), pw_id)

    token = get_access_token(args.tenant_id, args.app_id, app_secret, args.timeout, proxy)

    app_registration_secrets = get_app_registration_secrets(token, args.timeout, proxy)

    print("<<<check_graph_secrets:sep(0)>>>")
    print(json.dumps(app_registration_secrets))


if __name__ == "__main__":
    main()
