

#    --database ./database.csv --serverName X.Y.Z.W --serverPort 80 --registrations ./registrationsHome.csv

import csv
import concurrent.futures
import sys
import time
import threading

from datetime import datetime
from socket import *

from collections import defaultdict
from typing import List, Dict, Tuple, Any

DEBUG= False

####
# set server contact info
##
serverName= None
serverPort= None

####
# set my contact info
##
myIncomingPort= 1025

####
# variables that hold the contact information
# for the Arduino process registration server
##
clientSocket= None
serverSocket= None

#####
# number of threads for the responder server that pick off incoming client connections
# and service their requests.
###
numReqHandlerThreads= 5

#number of web request handler threads
numWebHandlerThreads= 2

#####
# registrations located in file
##
registrationFile= "registrations.csv"

#####
# Client requests and its lock
##
clientRequests= []
clientRequestLock= threading.Lock()

#####
# DataBase and its lock
##
databaseName= "database.csv"
databaseLock= threading.Lock()

databaseEntryLock= threading.Lock()
databaseEntries= []

webServerPort= 80
webRequests= []
webRequestLock= threading.Lock()

# Web request handler class

class WebRequestHandler:
    def __init__(self,connectionSock):
        self.connectionSock= connectionSock

    def get_request(self):
        self.incomingMsg= self.connectionSock.recv(1024)
        self.decodedMsg= self.incomingMsg.decode()
        print(f'WebRequestHandler::get_request:  received request from web client: {self.decodedMsg}\n')

    def close_connection(self):
        self.connectionSock.close()

    def get_message(self):
        return self.decodedMsg

#####
# Client request handler class
#
# Server side semantics for responder server interactions with responder
# client for data push
###
class ClientRequestHandler:
    def __init__(self,connectionSock):
        self.connectionSock= connectionSock

    def get_request(self):
        self.incomingMsg= self.connectionSock.recv(1024)
        self.decodedMsg= self.incomingMsg.decode()
        print(f'ClientRequestHandler::get_request:  received response from responder server: {self.decodedMsg}\n')

    def close_connection(self):
        self.connectionSock.close()

    def get_message(self):
        return self.decodedMsg



########
#  client_connect
#
#  registration client
#  initiate client connection to remote Arduino process's registration server using socket
###
def client_connect(clientSock, serverName, serverPort):
    clientSock.connect((serverName, serverPort))


########
# send_request
#
# registration client
# send registration request to remote Arduino process's registration server
###
def send_request(clientSock,req):
    print("registration client sending request: ")
    print(req + "\n")
    req= f"{req}\n"
    clientSock.send(req.encode())


########
# get_response
#
# registration client
# obtain response to registration request from remote Arduino process's registration server
###
def get_response(clientSock):
    response= clientSock.recv(1024)
    decodedResponse= response.decode()
    print(f'received response from registration server: {decodedResponse}\n')
    return decodedResponse


########
# request_handler
#
# responder server
# handles interaction with the responder client by receiving
# sensor measurement, constructing database entry, and appending
# it to list of entries
###
def request_handler():
    global clientRequestLock
    global databaseEntryLock
    global databaseEntries

    while True:
        clientRequestLock.acquire()

        if len(clientRequests) >0:
            clientRequest= clientRequests.pop(0)
        else:
            clientRequest= None

        clientRequestLock.release()

        if (clientRequest != None):
            clientRequest.get_request()
            theMessage= clientRequest.get_message()
            clientRequest.close_connection()

            # Parse message: soilmoisture,waterlevel,soil_value,water_value
            msgParts= theMessage.split(',')

            if len(msgParts) >= 4:
                # Create two separate entries for database
                soilEntry = [msgParts[0].strip(), 'any', msgParts[2].strip()]
                waterEntry = [msgParts[1].strip(), 'any', msgParts[3].strip()]
                
                sys.stdout.write(f'request_handler: soilEntry= {soilEntry}\n')
                sys.stdout.write(f'request_handler: waterEntry= {waterEntry}\n')

                databaseEntryLock.acquire()
                databaseEntries.append(soilEntry)
                databaseEntries.append(waterEntry)
                databaseEntryLock.release()

        else:
            time.sleep(1)

#sends the html
def send_response(connectionSock, html):
    connectionSock.send(html.encode())

def databaseAverages():
    """
    Reads a CSV file without headers, groups the data by the first two columns (0 and 1), 
    and calculates the average of the third column (index 2).
    """
    global databaseLock
    # Key: (factor, change) tuple -> Value: {'sum': total_value, 'count': num_entries}
    grouped_data = defaultdict(lambda: {'sum': 0.0, 'count': 0})

    databaseLock.acquire()
    
    with open('database.csv', mode='r', newline='') as file:
        reader = csv.reader(file, skipinitialspace=True)
        
        for row in reader:
            # Basic check for minimum required columns
            if len(row) < 3:
                continue

            factor: str = row[0].strip()
            change: str = row[1].strip()
            key: Tuple[str, str] = (factor, change)
            value: float = float(row[2].strip())
            
            # Update the sum and count for the group
            grouped_data[key]['sum'] += value
            grouped_data[key]['count'] += 1
            
    databaseLock.release()
    
    #Calculate Final Averages
    final_averages: List[Dict[str, Any]] = []
    for (factor, change), data in grouped_data.items():
        if data['count'] > 0:
            average: float = data['sum'] / data['count']
            final_averages.append({
                'Factor': factor,
                'Change': change,
                'Average Value': round(average, 2)
            })
            
    return final_averages

def getLatestReadings():
    """
    Gets the most recent reading for each sensor type
    """
    global databaseLock
    global databaseName
    latest_readings = {}
    
    databaseLock.acquire()
    
    try:
        print(f"DEBUG getLatestReadings: Opening {databaseName}")
        with open(databaseName, mode='r', newline='') as file:
            reader = csv.reader(file, skipinitialspace=True)
            all_rows = list(reader)
            
            print(f"DEBUG getLatestReadings: Found {len(all_rows)} rows")
            
            # Read from end to get latest values
            for row in reversed(all_rows):
                print(f"DEBUG getLatestReadings: Processing row: {row}")
                
                if len(row) < 3:
                    print(f"DEBUG getLatestReadings: Skipping - only {len(row)} columns")
                    continue
                    
                # Strip ALL whitespace from factor name
                factor = row[0].strip().replace(' ', '')
                
                print(f"DEBUG getLatestReadings: factor='{factor}', value='{row[2]}'")
                
                try:
                    value = float(row[2].strip())
                    
                    if factor not in latest_readings:
                        latest_readings[factor] = value
                        print(f"DEBUG getLatestReadings: Stored {factor} = {value}")
                except ValueError as e:
                    print(f"DEBUG getLatestReadings: ValueError - {e}")
                    continue
                    
                # Stop once we have both sensors
                if len(latest_readings) >= 2:
                    print("DEBUG getLatestReadings: Found both sensors, stopping")
                    break
                    
    except FileNotFoundError:
        print(f"DEBUG getLatestReadings: {databaseName} NOT FOUND!")
        pass
        
    databaseLock.release()
    
    print(f"DEBUG getLatestReadings: Returning {latest_readings}")
    return latest_readings

def web_request_handler():
    global webRequestLock

    while True:
        webRequestLock.acquire()

        if len(webRequests) > 0:
            webRequest = webRequests.pop(0)
        else:
            webRequest = None

        webRequestLock.release()

        if (webRequest != None):
            webRequest.get_request()
            message = webRequest.get_message()

            messageParts = message.split('\r\n')
            
            # Check if it's a valid HTTP request
            if len(messageParts) > 0 and ' ' in messageParts[0]:
                method, path, version = messageParts[0].split(' ')

                # Get latest readings
                latest = getLatestReadings()
                
                print(f"DEBUG: latest dictionary = {latest}")
                
                soil_moisture = latest.get('soilmoisture', '--')
                water_level = latest.get('waterlevel', '--')
                
                print(f"DEBUG: soil_moisture = {soil_moisture}, water_level = {water_level}")
                
                # Format with percentage sign if a number
                if soil_moisture != '--':
                    soil_moisture = f"{soil_moisture:.1f}%"
                if water_level != '--':
                    water_level = f"{water_level:.1f}%"
                
                print(f"DEBUG: After formatting - soil_moisture = {soil_moisture}, water_level = {water_level}") 
                
                html = f"""
                <html>
                <head>
                    <title>Plant Monitor</title>
                    <style>
                        body {{
                            font-family: Arial, sans-serif;
                            max-width: 600px;
                            margin: 50px auto;
                            padding: 20px;
                            background-color: #f0f0f0;
                        }}
                        .container {{
                            background: white;
                            border-radius: 10px;
                            padding: 30px;
                            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
                        }}
                        h1 {{
                            color: #2c3e50;
                            text-align: center;
                            margin-bottom: 30px;
                        }}
                        .reading {{
                            margin: 20px 0;
                            padding: 15px;
                            background: #ecf0f1;
                            border-radius: 5px;
                        }}
                        .label {{
                            font-size: 18px;
                            color: #7f8c8d;
                            margin-bottom: 5px;
                        }}
                        .value {{
                            font-size: 32px;
                            font-weight: bold;
                            color: #27ae60;
                        }}
                    </style>
                    <meta http-equiv="refresh" content="5">
                </head>
                <body>
                    <div class="container">
                        <h1>Plant Monitor</h1>
                        
                        <div class="reading">
                            <div class="label">Soil Moisture</div>
                            <div class="value">{soil_moisture}</div>
                            <div style="font-size: 14px; color: #95a5a6; margin-top: 5px;">Healthy range is between 25-50%</div>
                        </div>
                        
                        <div class="reading">
                            <div class="label">Water Level</div>
                            <div class="value">{water_level}</div>
                            <div style="font-size: 14px; color: #95a5a6; margin-top: 5px;">Minimum for Watering: 59%</div>
                        </div>
                    </div>
                </body>
                </html>
                """

                send_response(webRequest.connectionSock, html)
                webRequest.close_connection()

                sys.stdout.write(f'web_request_handler: served web page\n')
            else:
                webRequest.close_connection()

        else:
            time.sleep(1)

########
# create_responder_server
#
# create a server to receive the pushed data
###

def create_responder_server():
    serverSock = socket(AF_INET, SOCK_STREAM)
    serverSock.bind(('',myIncomingPort))
    serverSock.listen(1)
    print('create_responder_server:: Ready to receive data...')
    return serverSock

# create web server to receive HTTP requests
def create_web_server():
    webServerSock = socket(AF_INET, SOCK_STREAM)
    webServerSock.bind(('',webServerPort))
    webServerSock.listen(1)
    print('create_web_server:: Ready to receive data...')
    return webServerSock

########
# responder_request_handler
#
#  accept incoming connection, create handler object,
#  and append it to set of client connections
##

def responder_server_req_handler(serverSock):
    while True:
        connectionSocket, addr= serverSock.accept()
        clientRequest= ClientRequestHandler(connectionSocket)

        clientRequestLock.acquire()
        clientRequests.append(clientRequest)
        clientRequestLock.release()

# same as above but for web server
def web_server_req_handler(webServerSock):
    while True:
        connectionSocket, addr= webServerSock.accept()
        webRequest= WebRequestHandler(connectionSocket)

        webRequestLock.acquire()
        webRequests.append(webRequest)
        webRequestLock.release()

def responder_server():
    global serverSocket
    serverSocket= create_responder_server()
    responder_server_req_handler(serverSocket)


def csv_writer_thread(csvWriter,csvFile):
    global databaseLock
    global databaseEntries
    global databaseEntryLock

    while True:
        ## Thread safe get db entry
        databaseEntryLock.acquire()

        numDBEntries= len(databaseEntries)

        if (DEBUG):
            sys.stdout.write(f"csv_writer_thread: len(databaseEntries)= {numDBEntries}\n")

        if numDBEntries > 0:
            dbEntry= databaseEntries.pop(0)
        else:
            dbEntry= None

        databaseEntryLock.release()

        if (dbEntry != None):
            ## Thread safe write the database
            databaseLock.acquire()
            csvWriter.writerow(dbEntry)
            csvFile.flush()
            databaseLock.release()
        else:
            ##if nothing to write, sleep
            time.sleep(1)




def main(args):
    global databaseName
    global databaseLock
    global clientRequestLock
    global clientRequests
    global serverName
    global serverPort
    global registrationFile
    global clientSocket

    if len(args) == 8:
        if (args[0] == "--database") and (args[2] == "--serverName") and (args[4]=="--serverPort") and (args[6] == "--registrations"):
            databaseName= args[1]
            serverName= args[3]
            serverPort= int(args[5])
            registrationFile= args[7]
    else:
        sys.stdout.write(f"{sys.argv[0]} --database  filename.csv  --serverName x.y.z.w  --serverPort <integer>  --registrations filename.csv")
        exit(-1)

    #####
    # start the responder server
    ###
    serverThread= threading.Thread(target=responder_server,daemon= True)
    serverThread.start()

    #####
    # start the database writer
    ###
    file= open(databaseName,mode='a+')
    csvWriter= csv.writer(file,delimiter=',')

    csvWriterThread= threading.Thread(target=csv_writer_thread,args=(csvWriter,file),daemon= True)
    csvWriterThread.start()

    #start the web server
    webSock = create_web_server()
    webServerThread= threading.Thread(target=web_server_req_handler,args=(webSock,),daemon= True)
    webServerThread.start()
    ######
    # start the request handlers
    # these process incoming client requests placed on a queue by the
    # responder server
    ##
    requestHandlerThreads= []

    for i in range(numReqHandlerThreads):
        t= threading.Thread(target=request_handler)
        requestHandlerThreads.append(t)
        t.start()

    # start the web request handlers
    webHandlerThreads= []
    for i in range(numWebHandlerThreads):
        t= threading.Thread(target=web_request_handler)
        webHandlerThreads.append(t)
        t.start()
    ######
    # send registration request here
    ##
    regFile= open(registrationFile,mode='r')
    csvReader= csv.reader(regFile,delimiter=',')


    for row in csvReader:
        delayTime= int(row[0])
        theRegistration= row[1]
        sys.stdout.write(f"main:  new registration:  {theRegistration}\n")
        sys.stdout.write(f"main:  sleeping for {delayTime/1000} seconds...\n")
        time.sleep(delayTime/1000)
        clientSocket = socket(AF_INET, SOCK_STREAM)
        client_connect(clientSocket,serverName, serverPort)
        send_request(clientSocket, theRegistration )
        theResponse= get_response(clientSocket)
        clientSocket.close()


    for thread in requestHandlerThreads:
        thread.join()



if __name__ == "__main__":
    main(sys.argv[1:]) # Pass command-line arguments (excluding script name)