import time, os, influxdb_client, serial, threading
from influxdb_client import InfluxDBClient, Point, WritePrecision
from influxdb_client.client.write_api import SYNCHRONOUS, ASYNCHRONOUS, WriteOptions
from concurrent.futures import ThreadPoolExecutor
from typing import Final

# setting consts that can be customized

# baud rate. Prob not needed as 38400 is standard
MD1200BAUD: Final[int] = int(os.getenv("MD1200BAUD", "38400"))
# used if you want to run it on multiple JBODs
SERIALADAPTER: Final[str] = os.getenv("SERIALADAPTER", "/dev/ttyUSB0")
# Factor that defines how aggressive the temperature curve is
TEMP_FACTOR: Final[float] = float(os.getenv("TEMP_FACTOR", "19"))
# time between sending command to get temp and storing it. It's there to allow JBOD to answer
EPPYSLEEPY: Final[float] = float(os.getenv("EPPYSLEEPY", "1"))

LOW_FAN_TRSHD: Final[float] = float(os.getenv("LOW_FAN_TRSHD", "21"))
HIGH_FAN_TRSHD: Final[float] = float(os.getenv("HIGH_FAN_TRSHD", "40"))

GETTMPCMND: Final[str] = os.getenv("GETTMPCMND",  "_temp_rd")
SETFANCMND: Final[str] = os.getenv("SETFANCMND", "set_speed")

DEFOUTPRCNTG: Final[float] = float(os.getenv("DEFOUTPRCNTG", "24"))

MDSERIALTIMEOUT: Final[float] = float(os.getenv("MDSERIALTIMEOUT", "1"))

TEMPREADINTERVAL: Final[float] = float(os.getenv("TEMPREADINTERVAL", "15"))

# If True or yes then we good
TEMPSETING = os.getenv("TEMPSETING", "1").strip().lower() in ("1", "true", "yes", "on")

PROCESSTEMPWAITTIME: Final[float] = float(os.getenv("PROCESSTEMPWAITTIME", "0.75")) 

BACKOFFTIME: Final[float] = float(os.getenv("BACKOFFTIME", "5"))

INFLUX_MAX_RETRIES: Final[int] = int(os.getenv("INFLUX_MAX_RETRIES", "3"))

# INFLUXDB config
# should Influx be used?
USEINFLUX: Final[bool] = os.getenv("USEINFLUX", "True").strip().lower() in ("1", "true", "yes", "on")
# token = "apg1gysUeCcxdcRTMmosJTenbEppmUNi9rXlANDB2oNadBdWAu2GVTDc_q_dyo0iyYsckKaOvPRm6ba2NK0y_A=="
INFLUXTOKEN: Final[str] = str(os.getenv("INFLUX_TOKEN", "0"))
# bucket = "JBOD"
INFLUXBUCKET: Final[str] = str(os.getenv("INFLUX_BUCKET", "0"))
# org = "staging"
INFLUXORG: Final[str] = str(os.getenv("INFLUX_ORG", "0"))
# url = "http://localhost:8086"
INFLUXURL: Final[str] = str(os.getenv("INFLUX_URL", "0"))
# measurement = "MD1200"
INFLUXMEASUREMENT: Final[str] = str(os.getenv("INFLUX_MEASUREMENT", "0"))
# MACHINE_TAG = "CHONGUS1200"
MACHINE_TAG: Final[str] = str(os.getenv("INFLUX_MACHINE_TAG", "0"))
# LOCATION = "HQ"
LOCATION: Final[str] = str(os.getenv("INFLUX_LOCATION", "0"))
# INFLX_SEPARATE_POINTS = 0.1
# INFLUX_SEPARATE_POINTS = float(os.getenv("INFLUX_SEPARATE_POINTS"), 0.1)

# init
MDserial = serial.Serial(
    port=SERIALADAPTER,\
    baudrate=MD1200BAUD,\
    parity=serial.PARITY_NONE,\
    stopbits=serial.STOPBITS_ONE,\
    bytesize=serial.EIGHTBITS,\
    timeout=MDSERIALTIMEOUT)

# lastTempReading: float = time.time() 
setSpeedrcode = 21
MDtempDict: dict = {}
MDict: dict = {}
currentSerialUsage = threading.Lock()
fluxSending: bool = False
currentTime: float = 0
lastTempReading: float = 0
# Initialize InfluxDB client and influxdb API
# ---------------------UNCOMMENT-----------------------
if USEINFLUX:
    inflxdb_client = influxdb_client.InfluxDBClient(url=INFLUXURL, token=INFLUXTOKEN, org=INFLUXORG)
    write_api = inflxdb_client.write_api(write_options=SYNCHRONOUS)
    inflxdb_LeData: list = []
# ---------------------UNCOMMENT-----------------------


# Just a helper function for process_temps to avoid db errors on flapping network
def process_temps_dbsend(inpdatatosend) -> bool:
    if not USEINFLUX:
        return True
    try:
        write_api.write(bucket=INFLUXBUCKET, org=INFLUXORG, record=inpdatatosend)
        return True
    except Exception as e:
        print(f"Influx write error {e}", flush=True)
        return False



def getTemp() -> dict:
    global MDict, fluxSending

    
    with currentSerialUsage:
        MDserial.write(f"{GETTMPCMND}\n\r".encode())
        time.sleep(1)
        MDreturning = MDserial.read_until(" >").decode()

    

    # MDict = {}

    # Sanitise output
    MDsanit = MDreturning.splitlines()

    #if there is smth do smth
    if MDreturning:

        for line in MDsanit:
            
            if ">" in line or "b'" in line:
                continue

            matchstm = line[2:6]

            match matchstm:
                case "BP_1":
                    MDict["bp1"] = int(line[12:14])
                case "BP_2":
                    MDict["bp2"] = int(line[12:14])
                case "SIM0":
                    MDict["sim0"] = int(line[12:14])
                case "SIM1":
                    MDict["sim1"] = int(line[12:14])
                case "EXP0":
                    MDict["exp0"] = int(line[12:14])
                case "EXP1":
                    MDict["exp1"] = int(line[12:14])
                # case "AVG":
                    # MDict["avg"] = int(line[12:14])
                    # MDict["avg"] = int(line.strip().split("=")[1].strip().replace("c", ""))
                    # try:
                    #     # Extract number from e.g. '  AVG = 40c'
                    #     temp = int(line.strip().split("=")[1].strip().replace("c", ""))
                    #     MDict["avg"] = temp
                    # except Exception as e:
                    #     # print(f"[WARN] Failed to parse AVG line: {line} ({e})", flush=True)
                    #     pass
                case _:
                    # try to catch the AVG line like: "  AVG = 40c"
                    stripped = line.strip()
                    if stripped.startswith("AVG"):
                        try:
                            temp = int(stripped.split("=")[1].strip().replace("c", ""))
                            MDict["avg"] = temp
                        except Exception as e:
                            print(f"[WARN] Could not parse AVG line: {line} ({e})", flush=True)
                            continue                    
                    # continue
    
        
        # {'bp1': 35, 'bp2': 29, 'sim0': 35, 'sim1': 33, 'exp0': 56, 'exp1': 54, 'avg': 40}
        # process_temps(MDict)
        fluxSending = True

        return MDict
    else:
        return {"error": "unidentified"}


def setSpeed(inSpeeDict: dict) -> int:

    bpavrg = 0
    # default
    outfanprcntg = 0

    # get backplanbe average 
    if "bp1" in inSpeeDict and "bp2" in inSpeeDict:
        bpavrg = (inSpeeDict["bp1"] + inSpeeDict["bp2"]) /2
        #outfanprcntg = int((bpavrg / (HIGH_FAN_TRSHD - LOW_FAN_TRSHD)) * TEMP_FACTOR)
        outfanprcntg = int((bpavrg / (HIGH_FAN_TRSHD - LOW_FAN_TRSHD)) * TEMP_FACTOR)
        # os.system(f"echo setting {outfanprcntg}%")

    with currentSerialUsage:

        # Set fan speed
        if outfanprcntg >= 20:
            MDserial.write((f"{SETFANCMND} {str(outfanprcntg)} \n\r").encode())  
            print(f"setting {outfanprcntg}%", flush=True)

            return outfanprcntg
        else:
            # Set default value
            MDserial.write((f"{SETFANCMND} {str(DEFOUTPRCNTG)} \n\r").encode())  

            return outfanprcntg
    
    
    # If something goes super wrong
    return 1



# Check if UART is used
# Not neede because when defining MDserial it gets automatically opened
# Will leave it here anyway
# try:
#     MDserial.open()
# except serial.serialutil.SerialException:
#     # MDserial.close()
#     # MDserial.open()
#     print("Port allready opened.\nTry closing it first")

# Threaded flow processor
def process_temps() -> None:
    global MDict, fluxSending, setSpeedrcode

    while True:
        # ---LeData---
        # {'bp1': 35, 'bp2': 29, 'sim0': 35, 'sim1': 34, 'exp0': 56, 'exp1': 54}
        # ---LeData---
        
        if fluxSending:
        # Prep InfluxDB data

            # build local copy in case MDict changes while executing
            MDictLocalCopy = MDict.copy()
            try:
                inflxdb_Data_To_Send = (
                    influxdb_client.Point(f"{INFLUXMEASUREMENT}-script")
                    .tag("MACHINE", MACHINE_TAG)
                    .tag("LOCATION", LOCATION)
                    .field("Backplane1", MDictLocalCopy["bp1"])
                    .field("Backplane2", MDictLocalCopy["bp2"])
                    .field("SASIntModule0", MDictLocalCopy["sim0"])
                    .field("SASIntModule1", MDictLocalCopy["sim1"])
                    .field("Expander0", MDictLocalCopy["exp0"])
                    .field("Expander1", MDictLocalCopy["exp1"])
                    .field("Average", MDictLocalCopy["avg"])
                    .field("FanSpeed", setSpeedrcode)
                )
            except KeyError:
                time.sleep(BACKOFFTIME)
                continue

            # Prep/append data
            inflxdb_LeData.append(inflxdb_Data_To_Send)

            # Issue YuruC3/MD1200#7 fix
            if not process_temps_dbsend(inflxdb_Data_To_Send):
                i = 0                
                while i < INFLUX_MAX_RETRIES:
                    if process_temps_dbsend(inflxdb_Data_To_Send):
                        print("Sending data to InfluxDB", flush=True)
                        break
                    else:
                        time.sleep(1)
                    i += 1
                else:
                   print(f"Failed to send data to InfluxDB after {INFLUX_MAX_RETRIES} retires", flush=True)

            else:
                print("Sending data to InfluxDB", flush=True)
            
                
            
            # Clean up before another lo#op, 0.75
            inflxdb_LeData.clear()

            fluxSending = False

        else:
            time.sleep(PROCESSTEMPWAITTIME)




# Init
MDict = getTemp()
lastTempReading = time.time()

def mainCodeHere() -> None:
    global setSpeedrcode
    while True:
        global MDict, fluxSending, currentTime, lastTempReading
        # https://stackoverflow.com/questions/52578122/not-able-to-send-the-enter-command-on-pyserial

        # get temperature data, wait for MD1200 to answer and store 

        currentTime = time.time()

        if currentTime - lastTempReading >= TEMPREADINTERVAL:

            getTemp()    
            lastTempReading = currentTime
        
        if MDict and TEMPSETING:
            setSpeedrcode = setSpeed(MDict)

            # good
            if setSpeedrcode == 0:
                pass
                # print("Were mint", flush=True)
                # time.sleep(EPPYSLEEPY)
            # not good
            elif setSpeedrcode == 1:
                print("Ambigous temperature readings.\nFalling back to safe values.", flush=True)
                # time.sleep(EPPYSLEEPY)
            # very not good
            elif setSpeedrcode == -1:
                print("o nyo", flush=True)
                exit()
            # very very very not good
            else:
                print("idk", flush=True)
                exit()
        elif TEMPSETING == False:
            print(f"Waiting to get temp", flush=True)
            pass
        else:
            print(f"temperature not yet pulled.\nFalling back do default fan speed", flush=True)
            # os.system(f"echo temperature not yet pulled.\nFalling back do default fan speed")
            
            with currentSerialUsage:
                MDserial.write((f"{SETFANCMND} {str(DEFOUTPRCNTG)} \n\r").encode()) 

            

        time.sleep(EPPYSLEEPY) 


# Prepare threads and launch them


# daemon to make docker exit smoother
thread_main = threading.Thread(target=mainCodeHere, daemon=True)
threads = [thread_main]

if USEINFLUX:
    thread_flux = threading.Thread(target=process_temps, daemon=True)
    threads.append(thread_flux)

for thr in threads:
    thr.start()

# Join both (this will block forever, which is fine for a daemon)
for thr in threads:
    thr.join()




