#!/usr/bin/env python3
import subprocess
import re
import sys
import datetime
import argparse
import configparser
import os

DEFAULT_EK = "0000000000000000000000000000000000000000"

# IPMI settings (loaded at runtime from credentials file)
IPMIUSER = None
IPMIPW = None
IPMIEK = DEFAULT_EK

# Fan / temperature settings
FANSPEED = 20   # percent
MAXTEMP = 37    # Celsius

def build_ipmi_base():
    base = ["ipmitool", "-I", IPMIINTERFACE, "-H", IPMIHOST, "-U", IPMIUSER, "-P", IPMIPW, "-y", IPMIEK]
    if IPMICIPHER is not None:
        base += ["-C", str(IPMICIPHER)]
    return base

def has_ipmi_session_error(stderr_text):
    s = (stderr_text or "").lower()
    markers = [
        "unable to establish ipmi v2 / rmcp+ session",
        "rakp",
        "session timeout",
        "authentication type",
        "invalid user name",
    ]
    return any(marker in s for marker in markers)

def run_ipmi(args):
    base = build_ipmi_base()
    cmd = base + args
    p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    if p.returncode != 0 or has_ipmi_session_error(p.stderr):
        raise RuntimeError(
            f"ipmitool failed: {' '.join(cmd)}\n"
            f"stdout: {(p.stdout or '').strip() or '<empty>'}\n"
            f"stderr: {(p.stderr or '').strip() or '<empty>'}"
        )
    return p.stdout

def run_ipmi_raw(args):
    base = build_ipmi_base()
    cmd = base + args
    p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    return cmd, p.returncode, p.stdout, p.stderr

def log_systemd(tag, msg):
    try:
        subprocess.run(["systemd-cat", "-t", tag], input=msg + "\n", text=True, check=True)
    except Exception:
        # fallback to stdout
        ts_print(msg)

def ts_print(msg):
    """ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
    now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    print(f"{now} {msg}")

def load_credentials(path):
    if not os.path.exists(path):
        raise RuntimeError(f"credentials file not found: {path}")

    config = configparser.ConfigParser()
    config.read(path)

    try:
        username = config.get("credentials", "username", fallback="root")
        password = config.get("credentials", "password")
        encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK)
    except (configparser.NoSectionError, configparser.NoOptionError) as e:
        raise RuntimeError(f"error reading credentials file: {e}")

    return username, password, encryption_key

def get_exhaust_temp():
    commands_to_try = [
        ["sensor", "get", "Exhaust Temp"],
        ["sdr", "type", "temperature"],
        ["sensor", "list"],
        ["sdr", "elist", "full"],
    ]

    debug_sections = []
    session_error_seen = False
    for args in commands_to_try:
        cmd, rc, out, err = run_ipmi_raw(args)
        if has_ipmi_session_error(err):
            session_error_seen = True
        cmd_text = " ".join(cmd)
        section = [
            f"Command: {cmd_text}",
            f"Return code: {rc}",
            "STDOUT:",
            out.strip() or "<empty>",
            "STDERR:",
            err.strip() or "<empty>",
        ]
        debug_sections.append("\n".join(section))

        # Parse explicit sensor-get output first.
        if args[:2] == ["sensor", "get"] and out:
            m = re.search(r"Sensor\s+Reading\s*:\s*(-?\d+(?:\.\d+)?)\b.*degrees\s*C", out, re.IGNORECASE)
            if m:
                return int(float(m.group(1)))

        # Parse table outputs that have pipe-delimited sensor rows.
        for line in out.splitlines():
            parts = [p.strip() for p in line.split("|")]
            if len(parts) < 2:
                continue

            sensor_name = parts[0]
            normalized_name = " ".join(sensor_name.lower().split())
            if not (normalized_name == "exhaust temp" or normalized_name.startswith("exhaust temp ")):
                continue

            for part in parts[1:]:
                m = re.search(r"(-?\d+(?:\.\d+)?)\s*degrees\s*C", part, re.IGNORECASE)
                if m:
                    return int(float(m.group(1)))

    if session_error_seen:
        raise RuntimeError(
            "Failed to establish an IPMI session. "
            "Check host/credentials, then try different options like "
            "--interface lan --cipher 3 or --interface lanplus --cipher 17.\n\n"
            + "Debug output from attempted commands:\n\n"
            + "\n\n---\n\n".join(debug_sections)
        )

    raise RuntimeError(
        "Failed to read Exhaust Temp from ipmitool output.\n"
        + "Debug output from attempted commands:\n\n"
        + "\n\n---\n\n".join(debug_sections)
    )

def main():
    # parse CLI args (IPMI host can be overridden with -H/--host)
    parser = argparse.ArgumentParser(description="Set fan speed via IPMI")
    parser.add_argument("-H", "--host", required=True, help="IPMI host/IP")
    parser.add_argument(
        "-c",
        "--credentials",
        default="/etc/credentials",
        help="Path to credentials file (default: %(default)s)",
    )
    parser.add_argument("--interface", default="lanplus", choices=["lanplus", "lan"], help="IPMI interface (default: %(default)s)")
    parser.add_argument("--cipher", type=int, default=None, help="Optional IPMI cipher suite ID (e.g. 3 or 17)")
    args = parser.parse_args()

    global IPMIHOST, IPMIINTERFACE, IPMICIPHER, IPMIUSER, IPMIPW, IPMIEK
    # set globals so run_ipmi() uses provided connection settings and credentials
    IPMIHOST = args.host
    IPMIINTERFACE = args.interface
    IPMICIPHER = args.cipher
    IPMIUSER, IPMIPW, IPMIEK = load_credentials(args.credentials)

    ts_print(f"{IPMIHOST}: Fan speed check for host {IPMIHOST}")
    try:
        temp = get_exhaust_temp()
        ts_print(f"{IPMIHOST}: Exhaust temperature: {temp} C")
    except Exception as e:
        ts_print(f"{IPMIHOST}: Error reading temperature: {e}")
        sys.exit(1)

    speed_hex = format(FANSPEED, "x")

    if temp > MAXTEMP:
        msg = f"{IPMIHOST}:Warning: Temperature is too high! Activating dynamic fan control! ({temp} C)"
        log_systemd("R710-IPMI-TEMP", msg)
        ts_print(msg)
        run_ipmi(["raw", "0x30", "0x30", "0x01", "0x01"])
    else:
        okmsg = f"{IPMIHOST}: Temperature is OK ({temp} C)"
        log_systemd("R710-IPMI-TEMP", okmsg)
        ts_print(okmsg)
        ts_print(f"{IPMIHOST}: Activating manual fan speeds! (requested %d%%)" % FANSPEED)
        run_ipmi(["raw", "0x30", "0x30", "0x01", "0x00"])
        run_ipmi(["raw", "0x30", "0x30", "0x02", "0xff", "0x" + speed_hex])

if __name__ == "__main__":
    main()
