#!/usr/bin/env python3

import argparse
import base64
import json
import os
import re
import sys
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen


def required_value(name, value):
    """Bricht mit einer verständlichen Meldung ab, wenn ein Pflichtwert fehlt."""
    if not value or not value.strip():
        raise ValueError(f"Der Wert {name} fehlt.")
    return value.strip()


def normalized_base_url(host):
    """Ergänzt bei Bedarf HTTPS und entfernt einen abschließenden Schrägstrich."""
    if not re.match(r"^https?://", host, re.IGNORECASE):
        host = f"https://{host}"
    if not re.match(r"^https?://[^/]+(?:/.*)?$", host, re.IGNORECASE):
        raise ValueError("PLANERIO_HOST muss einen HTTP(S)-Host enthalten.")
    return host.rstrip("/")


def request_json(url, headers, purpose):
    """Führt einen GET-Request aus und prüft Status sowie JSON-Antwort."""
    request = Request(url, headers=headers, method="GET")
    try:
        with urlopen(request, timeout=30) as response:
            return json.loads(response.read().decode("utf-8"))
    except HTTPError as error:
        raise RuntimeError(f"{purpose} ist mit HTTP-Status {error.code} fehlgeschlagen.") from error
    except URLError as error:
        raise RuntimeError(f"{purpose} konnte nicht erreicht werden.") from error
    except json.JSONDecodeError as error:
        raise RuntimeError(f"{purpose} hat keine gültige JSON-Antwort geliefert.") from error


def main():
    parser = argparse.ArgumentParser(description="Lädt Planerio-Schichten als JSON-Datei herunter.")
    parser.add_argument("--host")
    parser.add_argument("--username")
    parser.add_argument("--password")
    parser.add_argument("--shift-group-token")
    parser.add_argument("--start-date")
    parser.add_argument("--end-date")
    parser.add_argument("--output")
    arguments = parser.parse_args()

    # Werte aus der Kommandozeile überschreiben die gleichnamigen Umgebungsvariablen.
    host = normalized_base_url(required_value("PLANERIO_HOST bzw. --host", arguments.host or os.getenv("PLANERIO_HOST")))
    username = required_value("PLANERIO_TECHNICAL_USERNAME bzw. --username", arguments.username or os.getenv("PLANERIO_TECHNICAL_USERNAME"))
    password = required_value("PLANERIO_TECHNICAL_PASSWORD bzw. --password", arguments.password or os.getenv("PLANERIO_TECHNICAL_PASSWORD"))
    shift_group_token = required_value("PLANERIO_SHIFT_GROUP_TOKEN bzw. --shift-group-token", arguments.shift_group_token or os.getenv("PLANERIO_SHIFT_GROUP_TOKEN"))
    start_date = required_value("PLANERIO_START_DATE bzw. --start-date", arguments.start_date or os.getenv("PLANERIO_START_DATE"))
    end_date = required_value("PLANERIO_END_DATE bzw. --end-date", arguments.end_date or os.getenv("PLANERIO_END_DATE"))
    output = arguments.output or os.getenv("PLANERIO_JSON_OUTPUT", "schichten.json")

    if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", start_date) or not re.fullmatch(r"\d{4}-\d{2}-\d{2}", end_date):
        raise ValueError("Start- und Enddatum müssen das Format YYYY-MM-DD haben.")

    # Ausschließlich das technische Login verwenden, niemals das Planerio-Benutzerkonto.
    basic_auth = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii")
    token_response = request_json(
        f"{host}/partner-auth/v1/token",
        {"Authorization": f"Basic {basic_auth}", "Accept": "application/json"},
        "Das Abrufen des Session-Tokens",
    )
    session_token = token_response.get("tokenString") if isinstance(token_response, dict) else None
    if not session_token:
        raise RuntimeError("Die Token-Antwort enthält kein Session-Token.")

    query = urlencode({"startDate": start_date, "endDate": end_date, "format": "json"})
    shifts_url = (
        f"{host}/planerio/planning/shifts/api/external/"
        f"{quote(shift_group_token, safe='')}/shifts?{query}"
    )
    shifts = request_json(
        shifts_url,
        {"X-Planerio-API-Gateway-Auth": session_token, "Accept": "application/json"},
        "Der Abruf der Schichten",
    )
    if not isinstance(shifts, list):
        raise RuntimeError("Der Schichtabruf hat keine JSON-Liste geliefert.")

    # Die Einrückung erleichtert das Prüfen und Weiterverarbeiten der JSON-Datei.
    Path(output).write_text(json.dumps(shifts, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(f"Schichten wurden in {output} gespeichert.")


if __name__ == "__main__":
    try:
        main()
    except (OSError, RuntimeError, ValueError) as error:
        print(f"Fehler: {error}", file=sys.stderr)
        sys.exit(1)
