import os
import ast
from ast import literal_eval
import falcon
from falcon import HTTP_200, HTTP_400, HTTP_401, HTTP_500
import json
import logging
from dotenv import load_dotenv
from minio import Minio
from minio.error import S3Error
import io
import datetime
from datetime import timedelta, timezone
import jwt
import psycopg2
import html
import re
import traceback
import time
import pytz
from scipy import interpolate
from scipy import stats
import pandas as pd
from collections import defaultdict
import numpy as np
import warnings

import cv2

print("OpenCV path:", cv2.__file__)
print("Available OpenCV attributes:", dir(cv2))

try:
    # Create a small test array
    import numpy as np
    test_arr = np.zeros((10, 10), dtype=np.uint8)  # Small black square
    print("\nTesting cv2.imencode with simple array...")
    success, encoded = cv2.imencode('.png', test_arr)
    print("imencode test success:", success)
    print("Encoded data type:", type(encoded))
    print("Encoded data length:", len(encoded) if success else "N/A")
except Exception as e:
    print("imencode test failed with error:", str(e))
    print("Error type:", type(e).__name__)


EnablePlot = False

if EnablePlot:
    import matplotlib
    matplotlib.use('Agg')  # Set the backend before importing pyplot
    import matplotlib.pyplot as plt
    from matplotlib.colors import LinearSegmentedColormap

warnings.filterwarnings('ignore')
#print(np.__file__)

logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
logger.propagate = False
logger.setLevel(logging.WARNING) #.WARNING

handler = logging.StreamHandler()
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)  

location_names = {-1:"All",0:"?",5:"Office",6:"Hallway",7:"Garage",8:"Outside",9:"Conference Room",10:"Room",34:"Kitchen",
                  56:"Bedroom",78:"Living Room",102:"Bathroom",103:"Dining Room",104:"Bathroom Main",105:"Bathroom Guest",
                  106:"Bedroom Master", 107:"Bedroom Guest", 108:"Conference Room", 109:"Basement", 110:"Attic", 200:"Other"}

s_table = ["temperature", "humidity", "pressure", "light", "radar", "voc0", "voc1", "voc2", "voc3", "voc4", "voc5", "voc6", "voc7", "voc8", "voc9"]

location_indexes = {}

for i in location_names:    
    location_indexes[location_names[i]] = i

logger.warning("Well-api started")
load_dotenv()

DB_NAME = os.getenv('DB_NAME')
DB_USER = os.getenv('DB_USER')
DB_PASSWORD = os.getenv('DB_PASSWORD')
DB_HOST = os.getenv('DB_HOST')
DB_PORT = os.getenv('DB_PORT')

MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY')
MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY')
MINIO_HOST = "192.168.68.64"#os.getenv('MINIO_HOST')
MINIO_PORT = os.getenv('MINIO_PORT')
DAILY_MAPS_BUCKET_NAME = os.getenv('DAILY_MAPS_BUCKET_NAME')
JWT_SECRET = os.getenv('JWT_SECRET')
MASTER_ADMIN = os.getenv('MASTER_ADMIN')
MASTER_PS = os.getenv('MASTER_PS')

use_pdb = True
debug = True
debug_string = ""
logger.debug(f"Environment variables: {os.environ}")
filesDir = "/home/app/well_web_storage" #os.path.dirname(os.path.realpath(__file__))
min_io_address = MINIO_HOST + ":" + MINIO_PORT


logging.basicConfig(level=logging.DEBUG)

# Use the credentials we just created
#miniIO_blob_client = Minio(
    #"192.168.68.64:9000",
    #access_key="well_pipe",         # The user we created
    #secret_key="WellNuo_2024",      # The password we set
    #secure=False
#)
miniIO_blob_client = Minio(min_io_address, access_key=MINIO_ACCESS_KEY, secret_key=MINIO_SECRET_KEY, secure=False)

## Test operations
#try:
    ## First just try to list buckets
    #print("Attempting to list buckets...")
    #buckets = miniIO_blob_client.list_buckets()
    #print("Available buckets:")
    #for bucket in buckets:
        #print(f"- {bucket.name}")

    ## Try operations with the 'daily-maps' bucket
    #bucket_name = "daily-maps"
    
    ## Check if bucket exists
    #print(f"\nChecking if bucket '{bucket_name}' exists...")
    #if miniIO_blob_client.bucket_exists(bucket_name):
        #print(f"Bucket '{bucket_name}' exists")
        
        ## Try to put a small test object
        #print("\nTrying to upload test object...")
        #test_data = b"Hello MinIO!"
        #miniIO_blob_client.put_object(
            #bucket_name,
            #"test-file.txt",
            #io.BytesIO(test_data),
            #len(test_data)
        #)
        #print("Upload successful!")

#except S3Error as e:
    #print(f"\nS3Error occurred:")
    #print(f"Error code: {e.code}")
    #print(f"Error message: {e.message}")
    #print(f"Request ID: {getattr(e, 'request_id', 'N/A')}")
    #print(f"Bucket name: {getattr(e, 'bucket_name', 'N/A')}")

#except Exception as e:
    #print(f"\nUnexpected error: {type(e).__name__}")
    #print(f"Error message: {str(e)}")

user_id_2_user = {}
smell_min = 1
smell_max =  102400000 - 1
sensor_legal_values = {"radar": (0,1000, 1), "co2": (1, 15999, 31), "humidity": (1,99, 31), "light": (0, 4095, 1),
                       "pressure": (0, 10000, 5), "temperature": (1, 60, 31), "voc": (smell_min, smell_max, 31), "voc0": (smell_min, smell_max, 31),
                       "voc1": (smell_min, smell_max, 31), "voc2": (smell_min, smell_max, 31), "voc3": (smell_min, smell_max, 31), "voc4": (smell_min, smell_max, 31),
                       "voc5": (smell_min, smell_max, 31), "voc6": (smell_min, smell_max, 31), "voc7": (smell_min, smell_max, 31), "voc8": (smell_min, smell_max, 31), "voc9": (smell_min, smell_max, 31),
                       "s0": (smell_min, smell_max, 31), "s1": (smell_min, smell_max, 31), "s2": (smell_min, smell_max, 31), "s3": (smell_min, smell_max, 31), "s4": (smell_min, smell_max, 31),
                       "s5": (smell_min, smell_max, 31), "s6": (smell_min, smell_max, 31), "s7": (smell_min, smell_max, 31), "s8": (smell_min, smell_max, 31), "s9": (smell_min, smell_max, 31)}

def get_db_connection():
    return psycopg2.connect(dbname=DB_NAME, user=DB_USER, password=DB_PASSWORD, host=DB_HOST, port=DB_PORT)


def generate_token(username):
    expiration = datetime.datetime.now(timezone.utc) + timedelta(hours=24)
    token = jwt.encode({"username": username, "exp": expiration}, JWT_SECRET, algorithm="HS256")
    return token

def read_file(file_name, source = "LOCAL", type_ = "TEXT", bucket_name="daily-maps"):
    
    blob_data = ""
    if source == "MINIO":
        blob_data = ReadObjectMinIO(bucket_name, file_name)
    elif source == "LOCAL":        
        login_file = os.path.join(filesDir, file_name)
        login_file = login_file.replace("\\","/")
        logger.debug(f"Full file path: {login_file}")
        logger.debug(f"File exists: {os.path.exists(login_file)}")
        #print(login_file)
        if type_ == "TEXT":            
            with open(login_file, encoding="utf8") as f:
                blob_data = f.read()
        else:
            with open(login_file, 'rb') as f:
                blob_data = f.read()
        
    elif source == "AZURE":
        try:
            blob_data = ""#container_client.download_blob(file_name).readall()
        except Exception as err:
            logger.error("Not reading Azure blob "+str(err))  
            blob_data = ""
        return blob_data        
    else:
        pass
    return blob_data

def verify_token(token):
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
        return payload
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None

def ReadObjectMinIO(bucket_name, file_name):
    try:
        # Retrieve the object data
        response = miniIO_blob_client.get_object(bucket_name, file_name)

        # Read the data from response
        data_bytes = response.read()

        # Convert bytes to string and then load into a dictionary
        data_string = data_bytes.decode('utf-8')

        # Don't forget to close the response
        response.close()
        response.release_conn()

        return data_string

    except S3Error as e:
        logger.error(f"An error occurred: {e}")
        return None
    
def package_response(content, status=falcon.HTTP_200):
    """
    Format the HTTP response.
    
    :param content: The content to be returned in the response.
    :param status: HTTP status code (default is 200 OK).
    :return: A dictionary containing the formatted response.
    """
    if isinstance(content, str):
        # If content is a string, try to parse it as JSON
        try:
            response = json.loads(content)
        except json.JSONDecodeError:
            # If it's not valid JSON, use it as message
            response = {"message": content}
    elif isinstance(content, dict):
        # If content is a dictionary, serialize it with datetime handling
        try:
            # First serialize to JSON string with datetime handling
            json_str = json.dumps(content, default=datetime_handler)
            # Then parse back to dict
            response = json.loads(json_str)
        except TypeError as e:
            response = {"message": f"Serialization error: {str(e)}"}
    else:
        # For any other type, convert to string and use as message
        response = {"message": str(content)}
    
    # Add status code to the response
    response["status"] = status
    
    # Handle specific status codes
    if status == falcon.HTTP_400:
        response["error"] = "Bad Request"
    elif status == falcon.HTTP_401:
        response["error"] = "Unauthorized"
    elif status == falcon.HTTP_500:
        response["error"] = "Internal Server Error"
    
    return response

def GetPriviledges(conn, user_name, password):
    sql = "SELECT key, access_to_deployments, user_id FROM public.person_details WHERE user_name = '" + user_name + "'"

    with conn.cursor() as cur:
        cur.execute(sql)
        result = cur.fetchall()#cur.fetchone()
        if  result != None:            
            if result[0][0] == password:            
                return result[0][1], result[0][2]
            else:
                return "0", "0"
        else:
            return "0", "0"

def GetPriviledgesOnly(user):
    with get_db_connection() as conn:
        if isinstance(user, (int)) or user.isdigit():
            sql = "SELECT access_to_deployments FROM public.person_details WHERE user_id = " + user 
        else:        
            sql = "SELECT access_to_deployments FROM public.person_details WHERE user_name = '" + user + "'"
        
        with conn.cursor() as cur:
            cur.execute(sql)
            result = cur.fetchall()#cur.fetchone()
            if  result != None:                    
                return result[0][0]
            else:
                return "0"

def ListDeployments(priviledges, user_id):
    
    global user_id_2_user
    
    conn = get_db_connection()

    if priviledges == "-1":            
        sql = "SELECT * FROM public.deployments ORDER BY deployment_id ASC;"
    else:
        sql = f"SELECT * FROM public.deployments WHERE deployment_id IN ({priviledges}) OR user_edit = {user_id} ORDER BY deployment_id ASC;"
    
    try:
        
        with conn.cursor() as cur:
            cur.execute(sql)
            result = cur.fetchall()#cur.fetchone()
            if result == None:                     
                complete_result = []
            else:
                deployment_ids = []
                deployment_records_dict = {}
                for record in result:
                    deployment_id = record[0] 
                    deployment_ids.append(deployment_id)
                    deployment_records_dict[deployment_id] = record 
        
            sql = f"SELECT * FROM public.deployment_details WHERE deployment_id IN ({",".join(map(str, deployment_ids))}) ORDER BY deployment_id ASC;"
            cur.execute(sql)
            details_result = cur.fetchall()
    
            beneficiary_ids = []
                
            for record_details in details_result:
                if record_details[1] != None and record_details[1] not in beneficiary_ids:
                    beneficiary_ids.append(record_details[1])
            
            sql = f"SELECT * FROM public.person_details WHERE user_id IN ({",".join(map(str, beneficiary_ids))});"
            
                
            cur.execute(sql)
            user_id_2_user = {}
            users = cur.fetchall()#cur.fetchone()
            for usr_record in users:
                user_id_2_user[usr_record[0]] = usr_record
            
            complete_result = []    
            if details_result != None:                     
                for record_details in details_result:
                    deployment_record = deployment_records_dict[record_details[0]]
                    complete_record = {'deployment_id': record_details[0], 'beneficiary_id': record_details[1], 'caretaker_id': record_details[2],
                                       'owner_id': record_details[3], 'installer_id': record_details[4],
                                       'address_street': record_details[6], 'address_city': record_details[7], 'address_zip': record_details[8],
                                       'address_state': record_details[9], 'address_country': record_details[10],
                                       'devices': record_details[5], 'wifis': record_details[11], 'persons': deployment_record[4], 'gender': deployment_record[5],
                                       'race': deployment_record[6], 'born': deployment_record[7], 'pets': deployment_record[8], 'time_zone': deployment_record[3]
                                       }
                    complete_result.append(complete_record)
    except:
        logger.debug(f"Error: {traceback.format_exc()}")         
    return complete_result 

def ListCaretakers():
    
    conn = get_db_connection()
    sql = "SELECT * FROM public.person_details WHERE role_ids LIKE '%2%' ORDER BY last_name;" #2 is caretaker
    
    with conn.cursor() as cur:
        cur.execute(sql)
        result = cur.fetchall()#cur.fetchone()
        if result == None:                     
            result = []

    return result    
    
def ListBeneficiaries(privilidges, user_info):
    
    conn = get_db_connection()
    with conn.cursor() as cur:    
    
        if (privilidges == "-1"):        
            sql = "SELECT * FROM public.person_details WHERE role_ids LIKE '%1%' ORDER BY last_name;" #1 is beneficiary
        else:
            #we need to find beneficiaries from list of deployments
            sql = f"SELECT beneficiary_id FROM public.deployment_details WHERE deployment_id IN ({privilidges}) ORDER BY deployment_id ASC;"
            cur.execute(sql)
            result1 = cur.fetchall()#cur.fetchone()
            if result1 == None:                     
                result = []
                return result
            beneficiaries = ",".join(str(x[0]) for x in result1)
            sql = f"SELECT * FROM public.person_details WHERE user_id IN ({beneficiaries}) OR user_edit = {user_info} AND role_ids LIKE '%1%' ORDER BY last_name;" #1 is beneficiary
            logger.debug(f"sql= {sql}")

        cur.execute(sql)
        result = cur.fetchall()#cur.fetchone()
        if result == None:                     
            result = []

    return result

def UserDetails(user_id):
    
    conn = get_db_connection()
        
    sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'person_details';"
    with conn.cursor() as cur:
        cur.execute(sql)
        columns_names = cur.fetchall()
        
    sql = "SELECT * FROM public.person_details WHERE user_id = "+user_id
    
    caretaker_record = {}
    with conn.cursor() as cur:
        cur.execute(sql)
        result = cur.fetchone() #cur.fetchall()
        if result != None:
            cnt = 0
            for field in columns_names: 
                caretaker_record[field[0]] = result[cnt]
                cnt += 1
    
    return caretaker_record    

def DeploymentDetails(deployment_id):
    
    
    deployment_record = {}
    
    conn = get_db_connection()
    
    with conn.cursor() as cur:
        
        sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'deployment_details';"
        cur.execute(sql)
        columns_names = cur.fetchall()
                                
        sql = "SELECT * FROM public.deployment_details WHERE deployment_id = "+deployment_id
        
        cur.execute(sql)
        result = cur.fetchone() #cur.fetchall()
        if result != None:
            cnt = 0
            for field in columns_names: 
                deployment_record[field[0]] = result[cnt]
                cnt += 1
    
        sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'deployments';"
        cur.execute(sql)
        columns_names1 = cur.fetchall()
    
        sql = "SELECT * FROM public.deployments WHERE deployment_id = "+deployment_id

        cur.execute(sql)
        result = cur.fetchone() #cur.fetchall()
        if result != None:
            cnt = 0
            for field in columns_names1: 
                deployment_record[field[0]] = result[cnt]
                cnt += 1
 
    
    
    return deployment_record    

def ValidUser(user_name, password):


    if use_pdb:
        with get_db_connection() as db_conn:
            priviledges, user_id= GetPriviledges(db_conn, user_name, password)
            return priviledges, user_id
        
    else:
        pass
        #container = GetReference("/MAC")
        #try:
            ## We can do an efficient point read lookup on partition key and id
            ##response = container.read_item(item="64B708896BD8_temperature_2024-01-01_00", partition_key="64B708896BD8") #OK
            ##items = query_items(container, '64B708896BD8') #Too slow
            ##AddToLog("1!")
            #privileges = GetCaretakers(container, email, password)
            #return privileges

        #except Exception as err:
            #AddToLog("Error !1 "+str(err))  

def AddToLog(str_s):
    global debug_string
    if debug:
        try:
            if isinstance(str_s, str):
                debug_string = debug_string + str_s + "\n"
                logging.info(str_s)
            elif isinstance(str_s, (int, float)):
                debug_string = debug_string + str(str_s) + "\n"
                logging.info(str(str_s))
                #debug_string = debug_string + str(type(str_s)) + "\n"
            elif isinstance(str_s, (list, complex, dict)):
                debug_string = debug_string + str(str_s) + "\n"
                logging.info(str(str_s))
                #debug_string = debug_string + str(type(str_s)) + "\n"
            else:
                debug_string = debug_string + str(type(str_s)) + "\n"#"Err:Unknown type!\n"
                logging.info(str(str_s))

        except Exception as err:
            debug_string = debug_string + str(err) + "\n"
    else:
        print(str_s)

def SelectOption(html_code, select_id, selected_item):
    """
    Modifies HTML code to set the selected attribute for a specific option in a select element.
    
    Args:
        html_code (str): Original HTML code
        select_id (str): ID of the select element to modify
        selected_item (str or int): Value of the option to be selected
    
    Returns:
        str: Modified HTML code with the selected attribute added
    """
    # Convert selected_item to string for comparison
    selected_item = str(selected_item)
    
    # Find the select element with the given ID
    select_pattern = rf'<select[^>]*id=[\'"]?{select_id}[\'"]?[^>]*>(.*?)</select>'
    select_match = re.search(select_pattern, html_code, re.IGNORECASE | re.DOTALL)
    
    if not select_match:
        return html_code  # Return unchanged if select element not found
    
    select_content = select_match.group(0)
    select_content_orig = select_content 
    # Remove any existing selected attributes
    select_content = re.sub(r'\s+selected(?=[>\s])', '', select_content, flags=re.IGNORECASE)
    
    # Add selected attribute to the matching option
    def replace_option(match):
        value = re.search(r'value=[\'"]?([^\'">\s]+)', match.group(0))
        if value and value.group(1) == selected_item:
            # Add selected attribute before the closing >
            return match.group(0).rstrip('>') + ' selected>'
        return match.group(0)
    
    modified_select = re.sub(
        r'<option[^>]*>', 
        replace_option, 
        select_content
    )
    
    # Replace the original select element with the modified one
    return html_code.replace(select_content_orig, modified_select)

def FillFields(blob_data, record, form_type):
    """
    Fill in the input fields in the HTML blob_data with values from the caretaker dictionary.
    
    :param blob_data: str - The initial HTML string containing empty or placeholder input fields.
    :param caretaker: dict - The dictionary containing values to populate the fields.
    :return: str - The HTML string with the input fields filled with the appropriate values.
    """
    # Ensure blob_data is a string
    #blob_data = str(blob_data)

    # Populate the fields
    for field in record:
        logger.debug(f"field= {field}")
        if field == "user_id":            
            if record[field] is not None:
                escaped_string = html.escape(str(record[field]))
                # Create a regex pattern to match the span with specific id
                pattern = rf'(<span[^>]+id="editing_user_id"[^>]*>)([^<]*)(</span>)'            
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(3)}', blob_data)
        elif field == "deployment_id":            
            if record[field] is not None:
                escaped_string = html.escape(str(record[field]))
                # Create a regex pattern to match the span with specific id
                pattern = rf'(<span[^>]+id="editing_deployment_id"[^>]*>)([^<]*)(</span>)'            
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(3)}', blob_data)
                
        elif field == "user_name":
            if record[field] != None:                
                escaped_string = html.escape(record[field])
                pattern = rf'(<input[^>]+id="new_user_name"[^>]+value=")[^"]*(")'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(2)}', blob_data)
                
                # Add value attribute if it does not exist
                pattern = rf'(<input[^>]+id="new_user_name"[^>]*)(>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)} value="{escaped_string}"{m.group(2)}', blob_data)

        elif field == "gender":
            if record[field] != None:                
                blob_data = SelectOption(blob_data, 'gender', record[field])
                                
        elif field == "race":
            if record[field] != None:                
                blob_data = SelectOption(blob_data, 'race', record[field])

        elif field == "time_zone_s":
            if record[field] != None:                
                blob_data = SelectOption(blob_data, 'time_zone_s', record[field])
            
        elif field == "time_edit" or field == "user_edit":
            pass
        else:
            if record[field] != None:                
                escaped_string = html.escape(str(record[field]))
                pattern = rf'(<input[^>]+id="{field}"[^>]+value=")[^"]*(")'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(2)}', blob_data)
                
                # Add value attribute if it does not exist
                pattern = rf'(<input[^>]+id="{field}"[^>]*)(>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)} value="{escaped_string}"{m.group(2)}', blob_data)

    return blob_data

def StoreBeneficiary2DB(parameters, editing_user_id):
    
    #print('\nCreating create_caretaker\n')
    # Create a caretaker object. This object has nested properties and various types including numbers, DateTimes and strings.
    # This can be saved as JSON as is without converting into rows/columns.
    conn = get_db_connection()
    cur = conn.cursor()

    if editing_user_id == None or editing_user_id == "":
        editing_user_id = "0"
        
    try:
        
        current_utc_time = datetime.datetime.now(timezone.utc)
        
        # Convert to epoch time
        current_epoch_time = current_utc_time.timestamp()        
        
        if editing_user_id != "0":
            sql = f"""
            UPDATE public.person_details
            SET 
                email = '{CleanObject(parameters.get('email'))}', 
                user_name = '{CleanObject(parameters.get('new_user_name'))}',
                first_name = '{CleanObject(parameters.get('first_name'))}',
                last_name = '{CleanObject(parameters.get('last_name'))}',
                address_street = '{CleanObject(parameters.get('address_street'))}',
                address_city = '{CleanObject(parameters.get('address_city'))}',
                address_zip = '{CleanObject(parameters.get('address_zip'))}',
                address_state = '{CleanObject(parameters.get('address_state'))}',
                address_country = '{CleanObject(parameters.get('address_country'))}',
                time_edit = {current_epoch_time},
                user_edit = {CleanObject(parameters.get('user_id'))},
                role_ids = '{CleanObject(parameters.get('role_ids'))}',
                phone_number = '{CleanObject(parameters.get('phone_number'))}',
                picture = '{CleanObject(parameters.get('picture'))}',
                key = '{CleanObject(parameters.get('key'))}'
            WHERE user_id = {editing_user_id};  -- replace 34 with the actual person_id you want to update
            """            
            
        else:
            sql = f"""
            INSERT INTO public.person_details 
            (role_ids, email, user_name, first_name, last_name, address_street, address_city, address_zip, address_state, address_country, time_edit, user_edit, phone_number, picture, key)
            VALUES 
            ('{CleanObject(parameters.get('role_ids'))}', '{CleanObject(parameters.get('email'))}', '{CleanObject(parameters.get('new_user_name'))}',
            '{CleanObject(parameters.get('first_name'))}', '{CleanObject(parameters.get('last_name'))}', '{CleanObject(parameters.get('address_street'))}',
            '{CleanObject(parameters.get('address_city'))}', '{CleanObject(parameters.get('address_zip'))}', '{CleanObject(parameters.get('address_state'))}',
            '{CleanObject(parameters.get('address_country'))}', {current_epoch_time}, {CleanObject(parameters.get('user_id'))}, '{CleanObject(parameters.get('phone_number'))}',
            '{CleanObject(parameters.get('picture'))}', '{CleanObject(parameters.get('key'))}');
            """ 
        logger.debug(f"sql= {sql}")
        # Execute update query
        cur.execute(sql)
        
        # Commit the changes to the database
        conn.commit()
        
        # Close the cursor and connection
        cur.close()
        conn.close()

        AddToLog("Written/updated!") 
        return 1
    except Exception as err:
        AddToLog(traceback.format_exc())        
        return 0  
    return ok   

def DeleteRecordFromDB(form_data):
    conn = get_db_connection()
    cur = conn.cursor()
    function = form_data.get('function')
    if function == "deployment_delete":
        user_id = form_data['user_id']
        editing_deployment_id = form_data['editing_deployment_id']
        priviledges = form_data['priviledges']
        if editing_deployment_id == None or editing_deployment_id == "" or editing_deployment_id == "0":
            AddToLog("deployment_id is not defined")
            cur.close()
            conn.close()        
            return 0         
        try:           
            if  user_id == "-1":            
                sql = f"""
                DELETE FROM public.deployments WHERE deployment_id = {editing_deployment_id}
                """            
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)
                
                sql = f"""
                DELETE FROM public.deployment_details WHERE deployment_id = {editing_deployment_id}
                """            
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)
                
                # Commit the changes to the database
                conn.commit()
                
                # Close the cursor and connection
                cur.close()
                conn.close()
        
                AddToLog("Deleted!") 
                return 1
            else:
                #lets check if user_edit = user_id
                sql = f"SELECT user_edit FROM public.deployments WHERE deployment_id = '{editing_deployment_id}'"
                cur.execute(sql)
                result = cur.fetchone()
                if priviledges != "-1":                    
                    if result[0] != int(user_id):
                        cur.close()
                        conn.close()
                        return 0
                
                sql = f"""
                DELETE FROM public.deployments WHERE deployment_id = {editing_deployment_id}
                """            
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)
                
                sql = f"""
                DELETE FROM public.deployment_details WHERE deployment_id = {editing_deployment_id}
                """            
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)
                
                # Commit the changes to the database
                conn.commit()
                
                # Close the cursor and connection
                cur.close()
                conn.close()
        
                AddToLog("Deleted!") 
                return 1            
                
        except Exception as err:
            AddToLog(traceback.format_exc())        
            cur.close()
            conn.close()
            return 0  
        
    else:
        
        user_id = form_data['user_id']
        editing_user_id = form_data['delete_user_id'] 
        if editing_user_id == None or editing_user_id == "" or editing_user_id == "0":
            AddToLog("user_id is not defined")
            cur.close()
            conn.close()        
            return 0         
        try:           
            if  user_id == "-1":            
                sql = f"""
                DELETE FROM public.person_details WHERE user_id = {editing_user_id}
                """            
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)
                
                # Commit the changes to the database
                conn.commit()
                
                # Close the cursor and connection
                cur.close()
                conn.close()
        
                AddToLog("Deleted!") 
                return 1
            else:
                #lets check if user_edit = user_id
                sql = f"SELECT user_edit FROM public.person_details WHERE user_id = '{editing_user_id}'"
                cur.execute(sql)
                result = cur.fetchone()            
                if result[0] != int(user_id):
                    cur.close()
                    conn.close()
                    return 0
                
                sql = f"""
                DELETE FROM public.person_details WHERE user_id = {editing_user_id}
                """            
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)
                
                # Commit the changes to the database
                conn.commit()
                
                # Close the cursor and connection
                cur.close()
                conn.close()
        
                AddToLog("Deleted!") 
                return 1            
                
        except Exception as err:
            AddToLog(traceback.format_exc())        
            cur.close()
            conn.close()
            return 0  


def StoreCaretaker2DB(parameters, editing_user_id):
    
    #print('\nCreating create_caretaker\n')
    # Create a caretaker object. This object has nested properties and various types including numbers, DateTimes and strings.
    # This can be saved as JSON as is without converting into rows/columns.
    conn = get_db_connection()
    cur = conn.cursor()

    if editing_user_id == None or editing_user_id == "":
        editing_user_id = "0"
        
    try:
        
        current_utc_time = datetime.datetime.now(timezone.utc)
        
        # Convert to epoch time
        current_epoch_time = current_utc_time.timestamp()        
        
        if editing_user_id != "0":
            sql = f"""
            UPDATE public.person_details
            SET 
                role_ids = '{parameters.get('role_ids')}', 
                access_to_deployments = '{parameters.get('access_to_deployments')}', 
                email = '{parameters.get('email')}', 
                user_name = '{parameters.get('new_user_name')}',
                first_name = '{parameters.get('first_name')}',
                last_name = '{parameters.get('last_name')}',
                address_street = '{parameters.get('address_street')}',
                address_city = '{parameters.get('address_city')}',
                address_zip = '{parameters.get('address_zip')}',
                address_state = '{parameters.get('address_state')}',
                address_country = '{parameters.get('address_country')}',
                time_edit = {current_epoch_time},
                user_edit = {parameters.get('user_id')},
                phone_number = '{parameters.get('phone_number')}',
                picture = '{parameters.get('picture')}',
                key = '{parameters.get('key')}'
            WHERE user_id = {editing_user_id};  -- replace 34 with the actual person_id you want to update
            """            
            
        else:
            sql = f"""
            INSERT INTO public.person_details 
            (role_ids, access_to_deployments, email, user_name, first_name, last_name, address_street, address_city, address_zip, address_state, address_country, time_edit, user_edit, phone_number, picture, key)
            VALUES 
            ('{parameters.get('role_ids')}', '{parameters.get('access_to_deployments')}', '{parameters.get('email')}', '{parameters.get('new_user_name')}', '{parameters.get('first_name')}', '{parameters.get('last_name')}', '{parameters.get('address_street')}', '{parameters.get('address_city')}', '{parameters.get('address_zip')}', '{parameters.get('address_state')}', '{parameters.get('address_country')}', {current_epoch_time}, {parameters.get('user_id')}, '{parameters.get('phone_number')}', '{parameters.get('picture')}', '{parameters.get('key')}');
            """ 
        logger.debug(f"sql= {sql}")
        # Execute update query
        cur.execute(sql)
        
        # Commit the changes to the database
        conn.commit()
        
        # Close the cursor and connection
        cur.close()
        conn.close()

        AddToLog("Written/updated!") 
        return 1
    except Exception as err:
        AddToLog(traceback.format_exc())        
        return 0  
    return ok

def CleanObject(object_in, typee = "s"):
    
    if typee == "n":
        res = 0
        if object_in == None or object_in == "":
            return 0
        if isinstance(object_in, str):
            try:        
                res = object_in.replace("'", '"')
            except:
                pass
            return res
    else:
        res = ""
        if object_in == None:
            return ""
        if isinstance(object_in, str):
            try:        
                res = object_in.replace("'", '"')
            except:
                pass
            return res
    return object_in

def StoreDeployment2DB(parameters, editing_deployment_id):
    conn = get_db_connection()
    cur = conn.cursor()

    if editing_deployment_id == None or editing_deployment_id == "":
        editing_deployment_id = "0"
        
    try:
                        
        current_utc_time = datetime.datetime.now(timezone.utc)
        
        # Convert to epoch time
        current_epoch_time = current_utc_time.timestamp()        
        
        if editing_deployment_id != "0":
            sql = f"""
            UPDATE public.deployments
            SET
                persons = {CleanObject(parameters.get('persons'), "n")},        
                gender = {CleanObject(parameters.get('gender'), "n")},
                race = {CleanObject(parameters.get('race'), "n")},
                born = {CleanObject(parameters.get('born'), "n")},
                pets = {CleanObject(parameters.get('pets'), "n")},
                time_zone_s = '{CleanObject(parameters.get('time_zone_s'))}',
                user_edit = {CleanObject(parameters.get('user_id'), "n")},
                time_edit = {current_epoch_time}
            WHERE deployment_id = {CleanObject(editing_deployment_id, "n")};
            """            
            
        else:
            sql = f"""
            INSERT INTO public.deployments 
            (persons, gender, race, born, pets, time_zone_s, user_edit, time_edit)
            VALUES 
            ({CleanObject(parameters.get('persons'), "n")}, {CleanObject(parameters.get('gender'), "n")}, {CleanObject(parameters.get('race'), "n")},
            {CleanObject(parameters.get('born'), "n")}, {CleanObject(parameters.get('pets'), "n")}, '{CleanObject(parameters.get('time_zone_s'))}',
            {CleanObject(parameters.get('user_id'), "n")}, {current_epoch_time})
            RETURNING deployment_id;
            """ 
        logger.debug(f"sql= {sql}")
        # Execute update query
        cur.execute(sql)
        if editing_deployment_id == "0":
            new_deployment_id = cur.fetchone()[0]
        # Commit the changes to the database
        conn.commit()                
        
        if editing_deployment_id != "0":
            sql = f"""
            UPDATE public.deployment_details
            SET
                beneficiary_id = {CleanObject(parameters.get('beneficiary_id'), "n")},
                caretaker_id = {CleanObject(parameters.get('caretaker_id'), "n")},
                owner_id = {CleanObject(parameters.get('owner_id'), "n")},
                installer_id = {CleanObject(parameters.get('installer_id'), "n")},        
                address_street = '{CleanObject(parameters.get('address_street'))}',
                address_city = '{CleanObject(parameters.get('address_city'))}',
                address_zip = '{CleanObject(parameters.get('address_zip'))}',
                address_state = '{CleanObject(parameters.get('address_state'))}',
                address_country = '{CleanObject(parameters.get('address_country'))}',
                
                wifis = '{CleanObject(parameters.get('wifis'))}',
                devices = '{CleanObject(parameters.get('devices'))}',
                lat = {CleanObject(parameters.get('lat'), "n")},
                lng = {CleanObject(parameters.get('lng'), "n")},
                gps_age = {CleanObject(parameters.get('gps_age'), "n")}
                    
            WHERE deployment_id = {editing_deployment_id};
            """            
            
        else:
            sql = f"""
            INSERT INTO public.deployment_details 
            (deployment_id, beneficiary_id, caretaker_id, owner_id, installer_id, address_street, address_city, address_zip, address_state, address_country)
            VALUES 
            ({new_deployment_id}, {CleanObject(parameters.get('beneficiary_id'), "n")}, {CleanObject(parameters.get('caretaker_id'), "n")}, {CleanObject(parameters.get('owner_id'), "n")}, {CleanObject(parameters.get('installer_id'), "n")},
            '{CleanObject(parameters.get('address_street'))}', '{CleanObject(parameters.get('address_city'))}', '{CleanObject(parameters.get('address_zip'))}', '{CleanObject(parameters.get('address_state'))}',
            '{CleanObject(parameters.get('address_country'))}');
            """ 
        logger.debug(f"sql= {sql}")
        # Execute update query
        cur.execute(sql)
        
        # Commit the changes to the database
        conn.commit()
        

        
        
        # Close the cursor and connection
        cur.close()
        conn.close()

        AddToLog("Written/updated!") 
        return 1
    except Exception as err:
        AddToLog(traceback.format_exc())        
        return 0  
    return ok    

def ShowAge(seconds):
    minutes = int(seconds // 60)
    hours = int(minutes // 60)
    days = int(hours // 24)
    
    if seconds >= 0:
        hours = int(hours - (days * 24))
        minutes = int(minutes - (days * 24 * 60) - (hours * 60))
        seconds = int(seconds - (days * 24 * 60 * 60) - (hours * 60 * 60) - (minutes * 60))
        
        if days > 0:
            report = f"{int(days)} d {int(hours)} h {int(minutes)} m {int(seconds)} s"
        elif hours > 0:
            report = f"{int(hours)} h {int(minutes)} m {int(seconds)} s"
        elif minutes > 0:
            report = f"{int(minutes)} m {int(seconds)} s"
        else:
            report = f"{int(seconds)} s"
            
        return report
    else:
        return "0 s"

def UpdateDevicesTable(html_string, devices, users):
    #ID, Well id, MAC,	Last_Message,	Location,	Description,	Deployment
    table_rows_string = ""
    for device in devices:
        result = next(item for item in users if item[0] == device[6])
        deployment_name = result[1]
        if result[2] != None:
            deployment_name = deployment_name + " " + result[2]
        mac = device[2] 
        mac_row_string = f'      <td style="text-align:center"><a href="google.com">{mac}</a></td>\n'   
        age = time.time() - device[3]

        if (age < 300):
            row_sting = f'    <tr style="background-color: #90FFD7;">\n      <th scope="row" style="text-align:center">{device[0]}</th>\n'
        else:
            row_sting = f'    <tr>\n      <th scope="row" style="text-align:center">{device[0]}</th>\n'
        
        row_ending = f'      <td style="text-align:center"><input type="checkbox" onchange="IsItSingle();"></td>\n    </tr>\n'
        for col_cnt in range(1, len(device)):
            column_value = device[col_cnt]
            #print(column_value)
            if col_cnt == 2:
                col_string_template = mac_row_string
            elif col_cnt == 3:
                if column_value > 0:                    
                    col_string_template = f'      <td style="text-align:center">{ShowAge(age)}</td>\n'
                else:
                    col_string_template = f'      <td style="text-align:center">No data</td>\n'
            elif col_cnt == 6:
                col_string_template = f'      <td style="text-align:center"><a href="#" onclick="OpenDeployment(\'{column_value}\')" title="{deployment_name}">{column_value}</a></td>\n';                
            else:
                if column_value == None:
                    column_value = ""
                col_string_template = f'      <td style="text-align:center">{column_value}</td>\n'  
            row_sting = row_sting + col_string_template
        row_sting = row_sting + row_ending
        table_rows_string = table_rows_string + row_sting
        #print(table_rows_string)

    html_string = html_string.replace("###ROWS###",table_rows_string)
    return html_string

def UpdateDeploymentsSelector(html_string, deployments, include_all=True, selected="1"):
    #                  <option value="All" selected>All</option>
    if include_all:
        selector_string = f'                  <option value="0">All</option>\n'
    else:
        selector_string = ''
        
    for deployment in deployments:
        first_name = ""
        last_name = ""
        if deployment[1] != None:
            first_name = deployment[1]
        if deployment[2] != None:
            last_name = deployment[2]
        if deployment[0] == int(selected):
            choice_string = f'                  <option value="{deployment[0]}" selected>{deployment[0]} {first_name} {last_name}</option>\n'
        else:
            choice_string = f'                  <option value="{deployment[0]}">{deployment[0]} {first_name} {last_name}</option>\n'
        selector_string =  selector_string + choice_string 
    #print(selector_string)

    html_string = html_string.replace("###INSTALLS###",selector_string)
    return html_string

def GetDeviceDetails(cur, deployment_ids, location_id):

    #ID, Well id, MAC,	Last_Message,	Location,	Description,	Deployment
    macs = [mac for _, mac in deployment_ids]
    #macs = list(deployment_ids.keys())
    macs_string_nq = ",".join(macs)
    macs_string = "'" + "','".join(macs) + "'"
    
    if location_id == -1:        
        sql = f"""
        WITH ordered_macs AS (
          SELECT unnest(string_to_array('{macs_string_nq}', ',')) as mac, 
                 generate_series(1, array_length(string_to_array('{macs_string_nq}', ','), 1)) as position
        )
        SELECT d.* 
        FROM public.devices d
        JOIN ordered_macs om ON d.device_mac = om.mac::text
        WHERE device_mac IN ({macs_string})
        ORDER BY om.position;
        """    
    else:
        sql = f"""
        WITH ordered_macs AS (
          SELECT unnest(string_to_array('{macs_string_nq}', ',')) as mac, 
                 generate_series(1, array_length(string_to_array('{macs_string_nq}', ','), 1)) as position
        )
        SELECT d.* 
        FROM public.devices d
        JOIN ordered_macs om ON d.device_mac = om.mac::text
        WHERE device_mac IN ({macs_string}) AND location = {location_id}
        ORDER BY om.position;
        """    

    cur.execute(sql)
    devices_ids_records = cur.fetchall()
    all_details = []

    
    devices_ids_list = [x[0] for x in devices_ids_records] 
    device_ids_string = ",".join(map(str, devices_ids_list))
    #sql = f"SELECT device_id, MAX(time) as last_reading_time FROM sensor_readings WHERE device_id IN ({device_ids_string}) GROUP BY device_id" #to slow
    sql = f"SELECT DISTINCT ON (device_id) device_id, time as last_reading_time FROM sensor_readings WHERE device_id IN ({device_ids_string}) ORDER BY device_id, time DESC"
    cur.execute(sql)
    devices_times = cur.fetchall()#cur.fetchone()
    found_device_details = {}
    for device_record in devices_times:
        device_id, last_message_time = device_record
        found_device_details[device_id] = last_message_time
    cnt = 0    
    for device_table_record in devices_ids_records:
        if len(devices_times) > 0:
            
            if device_id in found_device_details:
                last_message_time = found_device_details[device_id]
                last_message_epoch = int(last_message_time.timestamp())            
            else:
                last_message_time = int(device_table_record[14])
                last_message_epoch = last_message_time
        else:
            last_message_time = 0
            last_message_epoch = 0
            
        #print(last_message_epoch)
        #print(type(last_message_epoch))
        device_id = device_table_record[0]
        mac = device_table_record[1]
        well_id = device_table_record[2]
        description = device_table_record[3]
        location_id = device_table_record[4]
        row_data = [device_id, well_id, mac, last_message_epoch, location_names[location_id], description, deployment_ids[cnt][0]]
        cnt += 1
        all_details.append(row_data)
    
    return all_details
    
def GetVisibleDevices(deployments):

    devices_details = []

    with get_db_connection() as conn:
        #list all devices that user has access to
        if deployments == "-1":
            sql = "SELECT deployment_id, devices FROM public.deployment_details"
        else:            
            sql = f"SELECT deployment_id, devices FROM public.deployment_details WHERE deployment_id IN ({deployments})"
    
        with conn.cursor() as cur:
            cur.execute(sql)
            devices_groups = cur.fetchall()#cur.fetchone()
            deployment_ids = []
            for deployment_id, dev_group in devices_groups:
                if len(dev_group) > 10:
                    
                    macs_group = literal_eval(dev_group)
                    for mac in macs_group:
                        deployment_ids.append((deployment_id, mac))
    
            devices_details = GetDeviceDetails(cur, deployment_ids, -1)
            #devices_details.append(devices_detail)
        
    return devices_details

def GetVisibleDevicesPerLocation(deployments, location):

    devices_details = []

    with get_db_connection() as conn:
        #list all devices that user has access to
        if deployments == "-1" or deployments == "0":
            sql = "SELECT deployment_id, devices FROM public.deployment_details"
        else:            
            sql = f"SELECT deployment_id, devices FROM public.deployment_details WHERE deployment_id IN ({deployments})"
    
        with conn.cursor() as cur:
            cur.execute(sql)
            devices_groups = cur.fetchall()#cur.fetchone()
            deployment_ids = []
            for deployment_id, dev_group in devices_groups:
                if len(dev_group) > 10:                    
                    macs_group = literal_eval(dev_group)
                    for mac in macs_group:
                        deployment_ids.append((deployment_id, mac))
    
            devices_details = GetDeviceDetails(cur, deployment_ids, location_indexes[location])
            #devices_details.append(devices_detail)
        
    return devices_details

def GetUsersFromDeployments(deployments):
    #list all devices that user has access to
    deployments_dets = []
    with get_db_connection() as conn:
        try:
            if deployments == "-1":
                sql = f""" 
                SELECT dd.deployment_id, pd.first_name, pd.last_name
                FROM deployment_details dd
                JOIN person_details pd ON dd.beneficiary_id = pd.user_id                
                ORDER BY dd.deployment_id;
                """                
            else:                
                sql = f""" 
                SELECT dd.deployment_id, pd.first_name, pd.last_name
                FROM deployment_details dd
                JOIN person_details pd ON dd.beneficiary_id = pd.user_id
                WHERE dd.deployment_id IN ({deployments})
                ORDER BY dd.deployment_id;
                """
            with conn.cursor() as cur:
                cur.execute(sql)
                deployments_dets = cur.fetchall()#cur.fetchone()
        except Exception as err:
            logger.error("GetUsersFromDeployments "+str(err) +"  "+sql)          
            
    return deployments_dets

def ToList(input_data):
    # If input is already a list
    if isinstance(input_data, list):
        return [str(x).strip() for x in input_data]
    
    # If input is string
    if isinstance(input_data, str):
        # Remove outer brackets if present
        cleaned = input_data.strip('()')
        cleaned = cleaned.strip('[]')
        # Remove extra quotes
        cleaned = cleaned.replace('"', '').replace("'", '')
        # Split by comma and clean each element
        return [x.strip() for x in cleaned.split(',')]
        
    raise ValueError(f"Unsupported input type: {type(input_data)}")

def MACsToWellIds(cur, macs_list):
    
    macs_string = ",".join(f"'{mac}'" for mac in macs_list)
    sqlr = f"SELECT well_id, device_mac, device_id, location, description FROM public.devices WHERE device_mac IN ({macs_string})"

    print (sqlr)
    macs_map = {}
    cur.execute(sqlr)
    proximitys_list = cur.fetchall()
    for well_id, mac, device_id, location, description in proximitys_list: 
        macs_map[mac] = (well_id, device_id, location_names[location], description, mac)
    
    device_ids = []
    device_list = []
    for mac in macs_list:
        device_ids.append(macs_map[mac][1])
        device_list.append(macs_map[mac])
    
    return device_ids, device_list
    
def ReadCleanStringDB(cur, sql):    
    cur.execute(sql)
    temp_string = cur.fetchone()
    if temp_string == None:
        return ""
    else:
        return str(temp_string[0]).strip()

def GetProximityList(deployment_id, epoch_from_file_s):
    
    #both are valid:
    #64B70888FA84,64B70888F6F0,64B70888F860,64B70889062C,64B70888FAB0,64B708896BDC,64B708897428
    #['64B70888FA84', '64B70888F6F0', '64B70888F860', '64B70889062C', '64B70888FAB0', '64B708896BDC', '64B708897428']
    
    #result_list = []
    #well_ids = []
    with get_db_connection() as conn:

        sqlr = f"""
            SELECT * FROM (
                SELECT proximity 
                FROM public.deployment_history
                WHERE deployment_id = {deployment_id} 
                AND time <= {epoch_from_file_s} 
                ORDER BY time DESC 
                LIMIT 1
            ) AS latest_deployment
        """
        #print (sqlr)
        with conn.cursor() as cur:
            devices_string = ReadCleanStringDB(cur, sqlr)
        
            if devices_string == "":
                sqlr = f"SELECT devices from public.deployment_details WHERE deployment_id ={deployment_id}"
                #print (sqlr)
                devices_string = ReadCleanStringDB(cur, sqlr)
            
    
            if devices_string == "":
                return []
        
            macs_list = ToList(devices_string)
            device_ids, device_list = MACsToWellIds(cur, macs_list)
    return device_list, device_ids

def FilterList(to_filter: str, allowed: str) -> str:
    # Convert comma-separated strings to sets
    filter_set = set(to_filter.split(','))
    allowed_set = set(allowed.split(','))
    
    # Find intersection and sort the result
    filtered = sorted(filter_set.intersection(allowed_set), key=int)
    
    # Join back to comma-separated string
    return ','.join(filtered)

def GetMatchingDevices(privileges, group, deployment, location):

    global LocationsMap
    
    results=[]
    if privileges != "-1":
        if deployment == "" or deployment == "0":
            deployment = privileges

        privileges_list = privileges.split(',')
        if deployment != "0":
            if "," in  deployment:
                deployment = FilterList(deployment, privileges) 
            else:
                if deployment not in privileges_list:
                    return results
    else:
        if deployment == "0":
            deployment = "-1"
            
    devices = GetVisibleDevicesPerLocation(deployment, location)
    return devices   

def getOldestDeploymentHistoryFromBeneficiary(deployment_id):
    #this will return oldest entry as well as last proximity (devices)

    results=[]
    well_ids_last = [] #this needs to be list of tuples (well_id, Location_st, Description)
    oldest_time = None
    try:    
        with get_db_connection() as conn:    
            sqlr = f"""
                SELECT * FROM (
                    SELECT time, proximity 
                    FROM public.deployment_history
                    WHERE deployment_id = {deployment_id} 
                    ORDER BY time ASC
                ) AS latest_deployment
            """
            print (sqlr)
            with conn.cursor() as cur:
                cur.execute(sqlr)
                results = cur.fetchall()
                #lets find which of historical sets has data in DB
                if results == None or results == []: #look in deployment_details
                    sqlr = f"SELECT devices from public.deployment_details WHERE deployment_id ={deployment_id}"
                    #print (sqlr)
                    devices_string = ReadCleanStringDB(cur, sqlr)
                    macs_list = ToList(devices_string)
                    device_ids_last, device_alls_last = MACsToWellIds(cur, macs_list)                    
                    sql_query = """
                    SELECT MIN(time) as oldest_record_time
                    FROM sensor_readings
                    WHERE device_id = ANY(%s);
                    """
                    try:    
                        cur.execute(sql_query, (device_ids_last,))
                        results1 = cur.fetchall()
                        oldest_time = results1[0][0]                        
                    except:
                        pass

                else:
                    history_entry = results[-1]
                    macs_list = ToList(history_entry[1])
                    device_ids_last, device_alls_last = MACsToWellIds(cur, macs_list)                    
                    
                    
                    for history_entry in results:
                        macs_list = ToList(history_entry[1])
                        device_ids, device_alls = MACsToWellIds(cur, macs_list)
                        
                        sql_query = """
                        SELECT MIN(time) as oldest_record_time
                        FROM sensor_readings
                        WHERE device_id = ANY(%s);
                        """
                        try:    
                            cur.execute(sql_query, (device_ids,))
                            results1 = cur.fetchall()
                            oldest_time = results1[0][0]
                            if oldest_time != None:
                                break
                            
                            
                        except:
                            pass

    except Exception as e:
        AddToLog(traceback.format_exc())
    
    return oldest_time, device_alls_last

        
def getLastEditedBeneficiary(beneficiary):

    #lets generate token here to elliminate issues with outdated token...
    token = generate_token(beneficiary)
    url = 'https://well-api.azurewebsites.net/api/well_api'
    params = {
      "name": "beneficiary_detail",
      "beneficiary": beneficiary,
      "token": token
  }  
    #{"id": "user_beneficiary_bernhard@wellnuo.com", "MAC": "BENEFICIARY", "email": "bernhard@wellnuo.com", "edit_date": "Fri Aug 16 06:45:01 2024", "c_password": "bern1", "first_name": "Bernhard", "last_name": "Knigge", "address": "776 Dubanski Dr.", "address_city": "San Jose", "address_state": "CA", "address_zip": "95123", "address_country": "United States", "phone_number": "4087055709", "persons": "2", "gender": "M", "race": "W", "born": "1972", "pets": "1", "creds": "", "devs": "[[203, 'Living Room', '', '64B708890B14'], [251, 'Bathroom', '', '64B7088909E8'], [252, 'Bedroom', '', '64B708890734'], [204, 'Bathroom', 'Guest', '64B708890288'], [201, 'Kitchen', 'toaster', '64B708890584'], [202, 'Kitchen', 'stove', '64B7088906D8'], [205, 'Office', '', '64B708897018']]", "tzone": "America/Los_Angeles", "ttl": -1, "_rid": "R60hANIG-K+qTQIAAAAAAg==", "_self": "dbs/R60hAA==/colls/R60hANIG-K8=/docs/R60hANIG-K+qTQIAAAAAAg==/", "_etag": "\"3500a0ae-0000-0800-0000-66bef56d0000\"", "_attachments": "attachments/", "_ts": 1723790701}
    response = requests.get(url, params=params)
    if response.status_code == 200:

        text = response.text
        #print(text)
        if text == "Log-Out":
            return text
        if text[0] == "{":      
            data = json.loads(response.text)
            date_string = data["edit_date"]
            parsed_date = datetime.datetime.strptime(date_string, '%c')
            # Convert the datetime object to a timestamp (epoch time)
            epoch_str = str(time.mktime(parsed_date.timetuple()))
            devices = data["devs"]
            return(epoch_str, devices)
        else:
            return text,""
    else:
        logger.debug((f"Failed to retrieve the data, status code: {response.status_code}"))

    return "",""

def GetDeploymentNameFromId(Id):

    con = sqlite3.connect(main_db)
    con.text_factory = str
    cur = con.cursor()
    results=[]
    SQL = "SELECT name FROM deployments WHERE id =" + Id
    df = cur.execute(SQL)
    results = cur.fetchall()
    if len(results) > 0:    
        return results[0][0]
    else:
        return ""

def GetTimeZoneOfDeployment(deployment_id):
    time_zone_st = 'America/Los_Angeles'
    with get_db_connection() as conn:    
        with conn.cursor() as cur:
            sqlr = f"SELECT time_zone_s from public.deployments WHERE deployment_id ={deployment_id}"
            time_zone_st = ReadCleanStringDB(cur, sqlr)
    return time_zone_st

def StringToEpoch(date_string, time_zone_s):
    """
    Convert a date string to epoch timestamp for start of day (midnight) in specified timezone
    
    Args:
        date_string (str): Date in 'YYYY-MM-DD' format
        time_zone_s (str): Timezone string (e.g. 'America/Los_Angeles')
    
    Returns:
        float: Epoch timestamp in seconds
    """
    # Parse the date string
    date_format = '%Y-%m-%d'
    naive_date = datetime.datetime.strptime(date_string, date_format)
    
    # Get the timezone
    timezone = pytz.timezone(time_zone_s)
    
    # Localize the date to midnight in the specified timezone
    local_date = timezone.localize(naive_date)
    
    # Convert to epoch timestamp
    epoch_time = local_date.timestamp()
    
    return epoch_time

def LocalDateToUTCEpoch(local_date_str, time_zone_s):
    """
    Convert a date string to epoch timestamp for start of day (midnight) in UTC
    
    Args:
        local_date_str (str): Date in 'YYYY-MM-DD' format
        time_zone_s (str): Timezone string (e.g. 'America/Los_Angeles')
    
    Returns:
        float: Epoch UTC timestamp in seconds
    """
    timezone = pytz.timezone(time_zone_s)
    # Parse the date string
    date_format = '%Y-%m-%d'    
    local_datetime = datetime.datetime.strptime(local_date_str, date_format)
    local_datetime = timezone.localize(local_datetime)    
    
    utc_datetime = local_datetime.astimezone(pytz.UTC)
    epoch_time = int(utc_datetime.timestamp())
        
    return epoch_time

def GetDeploymentDatesBoth(deployment_in):

    #when looking at the date, date is defined in TZ where device is!
    #Lets take oldest data from first member of deployment
    date_list = []
    
    time_zone_st = GetTimeZoneOfDeployment(deployment_in)
    
    oldest_date_dt_utc, devices_all = getOldestDeploymentHistoryFromBeneficiary(deployment_in)

    if oldest_date_dt_utc != None:
        #get date in local time zone from UTC datetime
        
        #oldest_date_dt
        # Get today's date
        local_timezone = pytz.timezone(time_zone_st)  # Replace with your local timezone
        oldest_date_dt_local = oldest_date_dt_utc.astimezone(local_timezone)
        today_date = datetime.datetime.now(local_timezone)  

        # Generate a list of date strings from oldest_date to today in inverted order
        date_list = [(today_date - timedelta(days=x)).strftime('%Y-%m-%d') for x in range((today_date - oldest_date_dt_local).days + 1)]
    return date_list, devices_all

def check_file_exists(file_name, bucket_name="daily-maps"):
    try:
        # Try to get the object's stats - this will raise an exception if the object doesn't exist
        stat_result = miniIO_blob_client.stat_object(bucket_name, file_name)
        last_modified_utc = stat_result.last_modified
        return True, last_modified_utc
    except S3Error as e:
        if e.code == 'NoSuchKey':
            return False, 0
        # Re-raise if it's a different error
        raise
    
def SaveImageInBlob(file_name, arr_stretched):
    AddToLog(f"@@2")
    try:
        # Encode the image to a memory buffer using imencode
        success, encoded_image = cv2.imencode('.png', arr_stretched)
        AddToLog(f"success={success}")
        if not success:
            raise Exception("Could not encode image!")
        
        
        AddToLog(f"DAILY_MAPS_BUCKET_NAME={DAILY_MAPS_BUCKET_NAME}")
        
        image_bytes = encoded_image.tobytes()
        AddToLog(f"len(image_bytes)={len(image_bytes)}")
        miniIO_blob_client.put_object(
            DAILY_MAPS_BUCKET_NAME,
            file_name,
            io.BytesIO(image_bytes),
            len(image_bytes))        

    except Exception as e:
        AddToLog(f"{traceback.format_exc()}")
        logger.error(f"{traceback.format_exc()}")

def GetLocalTimeForDate(selected_date, time_zone_s):
    # Parse the selected date
    local_tz = pytz.timezone(time_zone_s)
    
    # Convert selected_date string to datetime object (start of day in local time)
    local_date = datetime.datetime.strptime(selected_date, "%Y-%m-%d")
    local_start = local_tz.localize(local_date)
    
    # Get the next day
    local_next = local_start + timedelta(days=1)
    
    # Convert to UTC
    utc_start = local_start.astimezone(pytz.UTC)
    utc_next = local_next.astimezone(pytz.UTC)
    
    # Format as strings
    time_from_str = utc_start.strftime("%Y-%m-%d %H:%M:%S")
    time_to_str = utc_next.strftime("%Y-%m-%d %H:%M:%S")
    
    return time_from_str + "+0000", time_to_str + "+0000"

def GetLocalTimeEpochsForDate(selected_date, time_zone_s):
    """
    Get start and end of day epochs for a given date in a specific timezone.
    
    Args:
        selected_date (str): Date in "YYYY-MM-DD" format
        time_zone_s (str): Timezone string (e.g., "America/New_York")
        
    Returns:
        tuple: (start_epoch, end_epoch) - Unix timestamps for start and end of day
    """
    # Parse the selected date
    local_tz = pytz.timezone(time_zone_s)
    
    # Convert selected_date string to datetime object (start of day in local time)
    local_date = datetime.datetime.strptime(selected_date, "%Y-%m-%d")
    local_start = local_tz.localize(local_date)
    
    # Get the next day
    local_next = local_start + timedelta(days=1)
    
    # Convert to UTC
    utc_start = local_start.astimezone(pytz.UTC)
    utc_next = local_next.astimezone(pytz.UTC)
    
    # Convert to epochs (Unix timestamps)
    start_epoch = int(utc_start.timestamp())
    end_epoch = int(utc_next.timestamp())
    
    return start_epoch, end_epoch

def UTC2Local(utc_time, time_zone_s):
    # Parse the selected date
    local_tz = pytz.timezone(time_zone_s)
    
    # Convert selected_date string to datetime object (start of day in local time)
    #local_date = datetime.datetime.strptime(selected_date, "%Y-%m-%d")
    local_start = local_tz.localize(selected_date)

    # Convert to UTC
    utc_start = local_start.astimezone(pytz.UTC)
    utc_next = local_next.astimezone(pytz.UTC)
    
    # Format as strings
    time_from_str = utc_start.strftime("%Y-%m-%d %H:%M:%S")
    time_to_str = utc_next.strftime("%Y-%m-%d %H:%M:%S")
    
    return time_from_str + "+0000", time_to_str + "+0000"

def get_timezone_aware_datetime(time_str, timezone_str="America/Los_Angeles"):
    """
    Convert a naive datetime string to a timezone-aware datetime object.
    
    Parameters:
    time_str: String in format 'YYYY-MM-DD HH:MM:SS'
    timezone_str: String representing the timezone (default: "America/Los_Angeles")
    
    Returns:
    datetime: A timezone-aware datetime object
    """
    # Parse the naive datetime
    naive_dt = datetime.datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S')
    
    # Get the timezone
    tz = pytz.timezone(timezone_str)
    
    # Localize the datetime (make it timezone-aware)
    # localize() is the correct way to do this, as it handles DST transitions properly
    aware_dt = tz.localize(naive_dt)    
    return aware_dt

def fast_fill_array_from_timescale(day_data, time_from_str, devices_list, arr_source, timezone_str="Europe/Berlin"):
    """
    Optimized version of array filling from TimeScaleDB data.
    Uses vectorized operations for significant speed improvement.
    """
    # Convert start time to timezone-aware datetime
    start_time = datetime.datetime.strptime(time_from_str, '%Y-%m-%d %H:%M:%S%z')
    #start_time = start_time.replace(tzinfo=timezone.utc)    
        
    # Create device index mapping
    device_to_index = {device_id: idx for idx, device_id in enumerate(devices_list)}
    
    # Define column mappings (sensor type to position in record)
    columns = {
        'avg_temperature': 2,
        'avg_humidity': 3,
        'pressure_amplitude': 4,
        'max_light': 5,
        'radar': 6
    }
    # Add sensor columns dynamically
    for i in range(10):
        columns[f'sensor_min_s{i}'] = i + 7
    
    # Pre-process data into a more efficient structure
    # Group by device_id to reduce lookup operations
    device_data = defaultdict(list)
    for record in day_data:
        if record[0] and record[1]:  # If time and device_id exist
            device_data[record[1]].append(record)
    
    # Process each device's data in bulk
    for device_id, records in device_data.items():
        if device_id not in device_to_index:
            continue
            
        base_idx = device_to_index[device_id] * len(columns)
        
        # Convert records to numpy array for faster processing
        records_array = np.array(records, dtype=object)
        
        # Calculate all minute deltas at once
        times = records_array[:, 0]
        #print(times[0], start_time, (times[0] - start_time).total_seconds())
        minute_deltas = np.array([(t - start_time).total_seconds() / 60 for t in times], dtype=int)
        
        # Filter valid minute deltas
        valid_mask = (minute_deltas >= 0) & (minute_deltas < arr_source.shape[1])
        if not np.any(valid_mask):
            continue
            
        minute_deltas = minute_deltas[valid_mask]
        records_array = records_array[valid_mask]
        
        # Process each column type in bulk
        for col_name, col_offset in columns.items():
            row_idx = base_idx + list(columns.keys()).index(col_name)
            values = records_array[:, col_offset]
            
            # Filter out None values
            valid_values = ~np.equal(values, None)
            if not np.any(valid_values):
                continue
                
            # Update array in bulk
            arr_source[row_idx, minute_deltas[valid_values]] = values[valid_values]
    
    return arr_source

def BestColor(in_val):
    #this function uses numbers from 0 to 1279 to convert to rainbow from Blue to Red(1024) to Violet 1279
    r,g,b=0,0,0
    in_val = int(in_val)
    if(in_val > 1279):
        in_val = 1279
    if (in_val < 256):
        r = 255
        g = in_val
    elif (in_val < 512):
        r = 511 - in_val
        g = 255
    elif (in_val < 768):
        g = 255
        b = in_val-512
    elif (in_val < 1024):
        g = 1023 - in_val
        b = 255
    else:
        r = in_val - 1024
        b = 255

    #if (r > 255):
    #    print(in_val)
    #    print(int(r),int(g),int(b))
    return(int(r),int(g),int(b))


def fill_array_from_timescale(day_data, time_from_str, devices_list, arr_source, timezone_str):
    """
    Fill numpy array with data from TimeScaleDB query results.
    
    Parameters:
    day_data: List of tuples from database query
    time_from_str: Starting datetime string in format 'YYYY-MM-DD HH:MM:SS'
    devices_list: List of device IDs
    arr_source: Pre-initialized numpy array to fill
    
    Returns:
    numpy.ndarray: Filled array
    """
    # Parse the start time
    start_time = get_timezone_aware_datetime(time_from_str, timezone_str)
    # Create mapping of device_ids to their index positions
    device_to_index = {device_id: idx for idx, device_id in enumerate(devices_list)}
    
    # Define columns and their positions in the result tuple
    columns = {
        'avg_temperature': 2,
        'avg_humidity': 3,
        'pressure_amplitude': 4,
        'max_light': 5,
        'radar': 6,
        'sensor_min_s0': 7,
        'sensor_min_s1': 8,
        'sensor_min_s2': 9,
        'sensor_min_s3': 10,
        'sensor_min_s4': 11,
        'sensor_min_s5': 12,
        'sensor_min_s6': 13,
        'sensor_min_s7': 14,
        'sensor_min_s8': 15,
        'sensor_min_s9': 16
    }
    
    # Process each record
    for record in day_data:
        # Get minute and device_id from record
        record_time = record[0]  # minute column
        device_id = record[1]    # device_id column
        
        if record_time and device_id:
            # Calculate minute delta
            minute_delta = int((record_time - start_time).total_seconds() / 60)
            
            if 0 <= minute_delta < arr_source.shape[1]:
                # Calculate base index for this device
                base_idx = device_to_index[device_id] * len(columns)
                
                # Fill data for each sensor/measurement type
                for col_name, col_offset in columns.items():
                    value = record[col_offset]
                    if value is not None:  # Skip NULL values
                        row_idx = base_idx + list(columns.keys()).index(col_name)
                        arr_source[row_idx, minute_delta] = value
    
    return arr_source

def CalcExtremes(arr_source, length, height):
    """
    Calculate min and max values for each row within legal bounds.
    Optimized version using numpy vectorized operations.
    
    Parameters:
    arr_source: numpy array of shape (height, length+4) containing data and bounds
    length: number of data points to process (typically 1440 for minutes in a day)
    height: number of rows in the array
    
    Returns:
    numpy array with min/max values stored in columns 1442 and 1443
    """
    # Extract the data portion and bounds
    data = arr_source[:, :length]
    ignore_below = arr_source[:, 1440:1441]  # Keep 2D shape for broadcasting
    ignore_above = arr_source[:, 1441:1442]  # Keep 2D shape for broadcasting
    
    # Create masks for valid values
    above_min_mask = data >= ignore_below
    below_max_mask = data <= ignore_above
    valid_mask = above_min_mask & below_max_mask
    
    # Create a masked array to handle invalid values
    masked_data = np.ma.array(data, mask=~valid_mask)
    
    # Calculate min and max values for each row
    row_mins = np.ma.min(masked_data, axis=1).filled(-0.001)
    row_maxs = np.ma.max(masked_data, axis=1).filled(-0.001)
    
    # Store results
    arr_source[:, 1442] = row_mins
    arr_source[:, 1443] = row_maxs
    
    return arr_source

def plot(arr, filename="multiple_rows.png", title="1D Array Plot", figsize=(12, 6),
                  color='blue', style='line'):
    """
    Plot a 1D numpy array as a line or scatter plot
    
    Parameters:
    arr : 1D numpy array
    title : str, plot title
    figsize : tuple, figure size in inches
    color : str, line/point color
    style : str, 'line' or 'scatter'
    """
    title = filename
    plt.figure(figsize=figsize)
    x = np.arange(len(arr))
    
    if style == 'line':
        plt.plot(x, arr, color=color)
    else:  # scatter
        plt.scatter(x, arr, color=color, alpha=0.6)
    
    plt.title(title)
    plt.xlabel('Index')
    plt.ylabel('Value')
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig(filename)
    plt.close()
    print(f"Plot saved to: {filename}")
    #plt.show()
    
    
def AddLimits(arr_source, devices_c, sensors_c, percentile):
    for y in range(devices_c*sensors_c):
        sensor_index = y % sensors_c
        min_ok, max_ok, window = sensor_legal_values[s_table[sensor_index]]
        if EnablePlot:
            if (y == 33):
                print("stop")
                plot(arr_source[y, :1440], "before_clean_sensor.png")
        if window > 2:            
            arr_source[y, :1440] = clean_data_vectorized(arr_source[y, :1440], window, percentile)

        if EnablePlot:
            if (y == 33):
                print("stop")
                plot(arr_source[y, :1440], "after_clean_sensor.png")

        arr_source[y][1440] = min_ok
        arr_source[y][1441] = max_ok
    return arr_source

def clean_data_vectorized(data, window, percentile):
    """
    Vectorized version of clean_data function using pure numpy
    First removes zeros, then cleans outliers
    
    Parameters:
    data: numpy array of sensor readings
    window: int, size of rolling window
    percentile: float, percentile threshold for deviation filtering
    """
    # Create a copy to avoid modifying original data
    working_data = data.copy()
    
    # Replace zeros with NaN
    zero_mask = working_data == 0
    working_data[zero_mask] = np.nan
    
    # Create rolling window view of the data
    def rolling_window(a, window):
        shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
        strides = a.strides + (a.strides[-1],)
        return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)
    
    # Pad array for edge handling
    pad_width = window // 2
    padded = np.pad(working_data, pad_width, mode='edge')
    
    # Create rolling windows
    windows = rolling_window(padded, window)
    
    # Calculate rolling median (ignoring NaN values)
    medians = np.nanmedian(windows, axis=1)
    
    # Forward/backward fill any NaN in medians
    # Forward fill
    mask = np.isnan(medians)
    idx = np.where(~mask, np.arange(mask.shape[0]), 0)
    np.maximum.accumulate(idx, out=idx)
    medians[mask] = medians[idx[mask]]
    
    # Backward fill any remaining NaNs
    mask = np.isnan(medians)
    idx = np.where(~mask, np.arange(mask.shape[0]), mask.shape[0] - 1)
    idx = np.minimum.accumulate(idx[::-1])[::-1]
    medians[mask] = medians[idx[mask]]
    
    # Calculate deviations (ignoring NaN values)
    deviations = np.abs(working_data - medians)
    
    # Calculate threshold (ignoring NaN values)
    threshold = np.nanpercentile(deviations, percentile)
    
    # Create mask and replace outliers with median values
    # Points are good if they're not NaN and deviation is within threshold
    good_points = (~np.isnan(working_data)) & (deviations <= threshold)
    
    # Replace all bad points (including zeros and outliers) with median values
    result = np.where(good_points, working_data, medians)
    
    return result


def process_chunk(args):
    """
    Process a chunk of rows
    """
    chunk, sensors_c, sensor_legal_values, s_table, window, percentile = args
    result = np.copy(chunk)
    
    # Process all time series in the chunk at once
    result[:, :1440] = np.array([
        clean_data_vectorized(row[:1440], window, percentile)
        for row in chunk
    ])
    
    # Set limits for all rows in chunk using vectorized operations
    sensor_indices = np.arange(len(chunk)) % sensors_c
    min_values = np.array([sensor_legal_values[s_table[i]][0] for i in sensor_indices])
    max_values = np.array([sensor_legal_values[s_table[i]][1] for i in sensor_indices])
    
    result[:, 1440] = min_values
    result[:, 1441] = max_values
    
    return result

   
def FillImage(scaled_day, devices_c, sensors_c, arr_stretched, group_by):
    """
    Fill the stretched array with colored sensor data.
    
    Parameters:
    scaled_day: 2D array of shape (stripes, minutes+4) containing sensor readings
    devices_c: number of devices
    sensors_c: number of sensors per device
    arr_stretched: 3D array of shape (stripes*stretch_by, minutes, 3) to fill with RGB values
    
    Returns:
    arr_stretched: Filled array with RGB values
    """
    stripes = devices_c * sensors_c
    stretch_by = arr_stretched.shape[0] // stripes
    minutes = arr_stretched.shape[1]
    
    # Create a boolean mask for VOC sensors
    if group_by != "sensortype":        
        voc_rows = np.array([i for i in range(stripes) if int(i/devices_c) >= 5])
    else:
        voc_rows = np.array([i for i in range(stripes) if int(i % sensors_c) >= 5])
    # Vectorize the BestColor function
    vectorized_best_color = np.vectorize(BestColor)
    
    # Process each row
    for row in range(stripes):
        
        row_data = scaled_day[row, :minutes]  # Get minute data

        #if row == 33:
        #    print("stop")
        #    plot(row_data, "row_data.png")
        big_min = scaled_day[row, 1442]  # min value
        big_max = scaled_day[row, 1443]  # max value
        
        # Create mask for valid values
        valid_mask = row_data != -0.001
        
        # Initialize RGB row with zeros
        rgb_row = np.zeros((minutes, 3), dtype=np.uint8)
        
        if big_max > big_min:
            # Scale factor
            k = 1280/(big_max-big_min)
            
            # Calculate normalized values
            normalized_vals = k * (row_data - big_min)
            
            # Invert if it's a VOC row
            if row in voc_rows:
                normalized_vals = 1279 - normalized_vals
            
            # Apply valid mask
            normalized_vals = np.where(valid_mask, normalized_vals, 0)
            #if row == 33:
            #    plot(normalized_vals, "normalized_vals.png") 
            
            # Convert to RGB colors (vectorized)
            r, g, b = vectorized_best_color(normalized_vals)
            
            # Combine into RGB array
            rgb_row[valid_mask] = np.stack([r[valid_mask], 
                                          g[valid_mask], 
                                          b[valid_mask]], axis=1)
        else:
            # Set to gray where valid
            rgb_row[valid_mask] = 128
        
        
        if group_by == "sensortype":
            
            # Fill the stretched rows
            sensor_index = row % sensors_c
            device_index = int(row/sensors_c)
            dest_row = sensor_index * devices_c + device_index  #0-0, 1-
            start_idx = dest_row * stretch_by
            end_idx = start_idx + stretch_by
            arr_stretched[start_idx:end_idx] = rgb_row
        else:
            # Fill the stretched rows
            start_idx = row * stretch_by
            end_idx = start_idx + stretch_by
            arr_stretched[start_idx:end_idx] = rgb_row
            
    return arr_stretched
    
def CreateMapFast(map_file, devices_list, selected_date, map_type, vocs_scaled, time_zone_s, radar_part, group_by):
    global Id2MACDict
    if radar_part == "s28":
        radar_part = "(s2+s3+s4+s5+s6+s7+s8)/7"
    try:
        
        #stretch_to_min_max = True
    
        #current_date_p = selected_date.replace("-", "_")
        #current_date_s = selected_date
    
        lower_than200 = 0
        larger_than200 = 0
        for details in devices_list[0]:
            dev_id =  details[0]
    
            if dev_id < 200:
                lower_than200 += 1
            else:
                larger_than200 += 1
    
        if lower_than200 > 0 and larger_than200 > 0:        
            return
    
        if larger_than200 > 0:
            sensors_c = len(s_table) 
        else: #old sensors not supported
            return False
    
        devices_c = len(devices_list[0])    
        devices_list_str = ",".join(map(str, devices_list[1]))    
        image_file = map_file
        time_from_str, time_to_str = GetLocalTimeForDate(selected_date, time_zone_s)
        
        sql = f"""
        SELECT 
            COALESCE(sr.minute, rr.minute) as minute,
            COALESCE(sr.device_id, rr.device_id) as device_id,
            sr.avg_temperature,
            sr.avg_humidity,
            sr.pressure_amplitude,
            sr.max_light,
            rr.radar,
            sr.min_s0 as sensor_min_s0,
            sr.min_s1 as sensor_min_s1,
            sr.min_s2 as sensor_min_s2,
            sr.min_s3 as sensor_min_s3,
            sr.min_s4 as sensor_min_s4,
            sr.min_s5 as sensor_min_s5,
            sr.min_s6 as sensor_min_s6,
            sr.min_s7 as sensor_min_s7,
            sr.min_s8 as sensor_min_s8,
            sr.min_s9 as sensor_min_s9        
        FROM (
            SELECT 
                time_bucket('1 minute', time) AS minute,
                device_id,
                AVG(temperature) AS avg_temperature,
                AVG(humidity) AS avg_humidity,
                AVG(pressure) AS pressure_amplitude,
                MAX(light) AS max_light,
                MIN(s0) AS min_s0,
                MIN(s1) AS min_s1,
                MIN(s2) AS min_s2,
                MIN(s3) AS min_s3,
                MIN(s4) AS min_s4,
                MIN(s5) AS min_s5,
                MIN(s6) AS min_s6,
                MIN(s7) AS min_s7,
                MIN(s8) AS min_s8,
                MIN(s9) AS min_s9
            FROM 
                sensor_readings
            WHERE 
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY 
                minute,
                device_id
        ) sr
        FULL OUTER JOIN (
            SELECT 
                time_bucket('1 minute', time) AS minute,
                device_id,
                MAX({radar_part}) AS radar
            FROM 
                radar_readings
            WHERE 
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY 
                minute,
                device_id
        ) rr
        ON sr.minute = rr.minute AND sr.device_id = rr.device_id
        ORDER BY 
            CASE COALESCE(sr.device_id, rr.device_id)
                WHEN 571 THEN 1
                WHEN 560 THEN 2
                WHEN 572 THEN 3
                WHEN 561 THEN 4
                WHEN 559 THEN 5
                WHEN 524 THEN 6
                WHEN 562 THEN 7
            END,
            COALESCE(sr.minute, rr.minute);  
        """
        #print(sql)
        #st = time.time()
        with get_db_connection() as conn:
            with conn.cursor() as cur:
                cur.execute(sql)
                day_data = cur.fetchall()#cur.fetchone()
                #print(result)
                if day_data == None:                    
                    return False
        #print(time.time() - st)
    
        #st = time.time()
    
        #creating image
        #print(time.time()-st)
    
        stretch_by = 10
        minutes = 1440
        stripes = devices_c * sensors_c #2 for upper maxes, lower mins
    
        arr_source_template = np.full((stripes, minutes+4), -0.001, dtype=float)
        
        #array to be written as image
        arr_stretched_template = np.zeros((int(stripes*stretch_by), minutes, 3), dtype=np.uint8)  # 3 for RGB channels
        #st = time.time()
        arr_source = fast_fill_array_from_timescale(day_data, time_from_str, devices_list[1], arr_source_template, time_zone_s)
        #plot(arr_source[33], "arr_source.png")         
        #print("@1:", time.time()-st)
        #st = time.time()
        #arr_source = process_data(arr_source, devices_c, sensors_c, window=5, percentile=99, use_threading=True)
        arr_source = AddLimits(arr_source, devices_c, sensors_c, percentile=99)
        #plot(arr_source[33][:1440], "arr_source2.png") 
        #print("@2:", time.time()-st)
        #st = time.time()
        #plot(arr_source[33], "arr_source3.png")
        scaled_day = CalcExtremes(arr_source, minutes, stripes)
        #plot(arr_source[33][:1440], "scaled_day.png")
        #print("@@1", time.time()-st)
        AddToLog(f"map_type={map_type}")
        AddToLog(f"type(map_type)={type(map_type)}")
        if True:#(int(map_type) == 1):
            #st = time.time()
            AddToLog(f"@@1")
            arr_stretched = FillImage(scaled_day, devices_c, sensors_c, arr_stretched_template, group_by)            
            #print("@@2", time.time()-st)            
            SaveImageInBlob(image_file, arr_stretched)
    
        return True
    except Exception as e:
        AddToLog(traceback.format_exc())                
        return False
    
def GetBlob(file_name):
    """
    Retrieve image from blob storage
    
    Args:
        file_name (str): Name of the file to retrieve from blob storage
        
    Returns:
        tuple: (image_bytes, content_type)
        Returns None, None if image not found or error occurs
    """
    AddToLog(f"GetBlob({file_name})")
    try:
        # Get the object from blob storage
        data = miniIO_blob_client.get_object(
            DAILY_MAPS_BUCKET_NAME,
            file_name
        )
        
        # Read the data into bytes
        image_bytes = data.read()
        AddToLog(f"len(image_bytes)={len(image_bytes)}")
        return image_bytes, 'image/png'
    except Exception as e:
        AddToLog(f"Error: {traceback.format_exc()}")
        logger.error(f"{traceback.format_exc()}")
        return None, None
    

def MapFileToDate(map_file):
    #'/Volumes/XTRM-Q/wellnuo/daily_maps/1/1_2023-11-07_dayly_image.png'
    parts = map_file.split("/")
    parts = parts[-1].split("_")

    if "-" in parts[0]:
        date_string = parts[0]
    elif "-" in parts[1]:
        date_string = parts[1]

    date_object = datetime.datetime.strptime(date_string, "%Y-%m-%d")
    date_only = date_object.date()
    return date_only

def GetMACsListSimple(list_of_lists):

    result = []
    if len(list_of_lists) > 0:
        result = [sublist[3] for sublist in list_of_lists]

    return(result)  

def datetime_handler(obj):
    """Handle datetime serialization for JSON"""
    if isinstance(obj, datetime.datetime):
        if obj.tzinfo:
            return obj.isoformat()
        return obj.strftime('%Y-%m-%d %H:%M:%S.%f')
    raise TypeError(f"Object of type {type(obj)} is not JSON serializable")

def ReadCandles(file, sensor, period, time_from, time_to):
    result = []
    if sensor == "voc0":
        sqlr = "SELECT * from vocs_0"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    elif sensor == "voc1":
        sqlr = "SELECT * from vocs_1"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to) 
    elif sensor == "voc2":
        sqlr = "SELECT * from vocs_2"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to) 
    elif sensor == "voc3":
        sqlr = "SELECT * from vocs_3"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to) 
    elif sensor == "voc4":
        sqlr = "SELECT * from vocs_4"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to) 
    elif sensor == "voc5":
        sqlr = "SELECT * from vocs_5"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to) 
    elif sensor == "voc6":
        sqlr = "SELECT * from vocs_6"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to) 
    elif sensor == "voc7":
        sqlr = "SELECT * from vocs_7"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to) 
    elif sensor == "voc8":
        sqlr = "SELECT * from vocs_8"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to) 
    elif sensor == "voc9":
        sqlr = "SELECT * from vocs_9"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to) 
    else:    
        sqlr = "SELECT * from "+sensor+"s"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    logger.debug(f"sqlr = {sqlr}")
    
    with get_db_connection() as conn:
        with conn.cursor() as cur:
            devices_string = ReadCleanStringDB(cur, sqlr)        
        result = QuerrySql(file, sqlr)
    return result

def ReadSensor(device_id, sensor, time_from_epoch, time_to_epoch, data_type, radar_part):
    
    time_utc = datetime.datetime.fromtimestamp(float(time_from_epoch), tz=timezone.utc)    
    # Format in ISO 8601 format with timezone
    time_from_str = time_utc.strftime("%Y-%m-%d %H:%M:%S%z")    

    time_utc = datetime.datetime.fromtimestamp(float(time_to_epoch), tz=timezone.utc)    
    # Format in ISO 8601 format with timezone
    time_to_str = time_utc.strftime("%Y-%m-%d %H:%M:%S%z")    
        
    legal_min, legal_max, window = sensor_legal_values[sensor]
    
    result = []
    if sensor == "radar":
        if radar_part == "s28":
            radar_part = "(s2+s3+s4+s5+s6+s7+s8)/7"
        sqlr = f"SELECT time, {radar_part} AS radar FROM radar_readings WHERE device_id = {device_id} AND time >= '{time_from_str}' AND time <= '{time_to_str}' ORDER BY time ASC"
    elif sensor[0] == "s":    
        sqlr = f"SELECT time, {sensor} AS smell FROM sensor_readings WHERE device_id = {device_id} AND {sensor} >= '{legal_min}' AND {sensor} <= '{legal_max}' AND time >= '{time_from_str}' AND time <= '{time_to_str}' ORDER BY time ASC"
    else:
        if sensor == "temperature":            
            sqlr = f"SELECT time, {sensor} - 16 from sensor_readings WHERE device_id = {device_id} AND {sensor} >= '{legal_min}' AND {sensor} <= '{legal_max}'  AND time >= '{time_from_str}' AND time <= '{time_to_str}' ORDER BY time ASC"
        else:
            sqlr = f"SELECT time, {sensor} from sensor_readings WHERE device_id = {device_id} AND {sensor} >= '{legal_min}' AND {sensor} <= '{legal_max}'  AND time >= '{time_from_str}' AND time <= '{time_to_str}' ORDER BY time ASC"         

    logger.debug(f"sqlr = {sqlr}")
    with get_db_connection() as conn:
        with conn.cursor() as cur:
            cur.execute(sqlr)
            result = cur.fetchall()

    return result

def check_and_parse(data_str):
    # Remove whitespace to handle cases with spaces
    cleaned = data_str.strip()
    # Check if second character is '['
    is_list_of_lists = cleaned[1] == '['
    if cleaned[0] == '[':
        # Parse the string regardless of type
        parsed = json.loads(cleaned)
    else:
        parsed = cleaned.split(",")
    return is_list_of_lists, parsed

def clean_data_with_rolling_spline(line_part_t, window=5, threshold=2.0):
    """
    Filter outliers using rolling median and replace with spline interpolation
    Returns data in the same format as input: [(timestamp, value), ...]
    """
    # Unzip the input tuples
    x, y = zip(*line_part_t)
    x = np.array(x)
    y = np.array(y, dtype=float)  # explicitly convert to float
    
    # Calculate rolling median and MAD using a safer approach
    rolling_median = []
    rolling_mad = []
    
    for i in range(len(y)):
        start_idx = max(0, i - window//2)
        end_idx = min(len(y), i + window//2 + 1)
        window_values = y[start_idx:end_idx]
        
        # Skip if window is empty or contains invalid values
        if len(window_values) == 0 or np.any(np.isnan(window_values)):
            rolling_median.append(y[i])
            rolling_mad.append(0)
            continue
            
        med = np.median(window_values)
        mad = np.median(np.abs(window_values - med))
        
        rolling_median.append(med)
        rolling_mad.append(mad)
    
    rolling_median = np.array(rolling_median)
    rolling_mad = np.array(rolling_mad)
    
    # Identify outliers (protect against division by zero)
    outlier_mask = np.abs(y - rolling_median) > threshold * (rolling_mad + 1e-10)
    good_data_mask = ~outlier_mask
    
    if np.sum(good_data_mask) < 4:
        return line_part_t  # return original data if we can't interpolate
    
    try:
        # Create and apply spline
        spline = interpolate.InterpolatedUnivariateSpline(
            x[good_data_mask], 
            y[good_data_mask],
            k=3
        )
        
        y_cleaned = y.copy()
        y_cleaned[outlier_mask] = spline(x[outlier_mask])
    except Exception as e:
        print(f"Spline interpolation failed: {e}")
        return line_part_t
    
    # Return in the same format as input
    return list(zip(x, y_cleaned))

def clean_data_with_spline(x, y, threshold=2.0):
    """
    Filter outliers and replace with spline interpolation
    
    Parameters:
    x : array-like, timestamps or x-coordinates
    y : array-like, values to be filtered
    threshold : float, number of median absolute deviations for outlier detection
    
    Returns:
    array-like : cleaned data with outliers replaced by spline interpolation
    """
    # Convert inputs to numpy arrays
    x = np.array(x)
    y = np.array(y)
    
    # Calculate median and median absolute deviation
    median = np.median(y)
    mad = stats.median_abs_deviation(y)
    
    # Identify outliers
    outlier_mask = np.abs(y - median) > threshold * mad
    good_data_mask = ~outlier_mask
    
    # If we have too few good points for interpolation, adjust threshold
    min_points_needed = 4  # minimum points needed for cubic spline
    if np.sum(good_data_mask) < min_points_needed:
        return y  # return original data if we can't interpolate
    
    # Create spline with non-outlier data
    spline = interpolate.InterpolatedUnivariateSpline(
        x[good_data_mask], 
        y[good_data_mask],
        k=3  # cubic spline
    )
    
    # Replace outliers with interpolated values
    y_cleaned = y.copy()
    y_cleaned[outlier_mask] = spline(x[outlier_mask])
    
    return y_cleaned

def clean_data(line_part_t, window=5, threshold=2.0):
    """
    Remove obvious outliers based on window comparison
    Returns cleaned data in the same format: [(timestamp, value), ...]
    """
    if len(line_part_t) < window:
        return line_part_t

    x, y = zip(*line_part_t)
    x = np.array(x)
    y = np.array(y, dtype=float)
    
    cleaned_data = []
    
    for i in range(len(y)):
        # Get window around current point
        start_idx = max(0, i - window//2)
        end_idx = min(len(y), i + window//2 + 1)
        window_values = y[start_idx:end_idx]
        
        # Calculate median and MAD for the window
        window_median = np.median(window_values)
        deviation = abs(y[i] - window_median)
        
        # Keep point if it's not too far from window median
        if deviation <= threshold * window_median:
            cleaned_data.append((x[i], y[i]))
        #else:
            #print(window_values)
    return cleaned_data

def clean_data_fast(line_part_t, window=5, threshold=2.0):
    """
    Remove obvious outliers based on window comparison - vectorized version
    Returns cleaned data in the same format: [(timestamp, value), ...]
    """
    if len(line_part_t) < window:
        return line_part_t

    x, y = zip(*line_part_t)
    x = np.array(x)
    y = np.array(y, dtype=float)
    
    # Calculate rolling median using numpy
    half_window = window // 2
    medians = np.array([
        np.median(y[max(0, i-half_window):min(len(y), i+half_window+1)])
        for i in range(len(y))
    ])
    
    # Calculate deviations for all points at once
    deviations = np.abs(y - medians)
    
    # Create mask for good points
    good_points = deviations <= threshold * medians
    
    # Return filtered data using boolean indexing
    return list(zip(x[good_points], y[good_points]))

def clean_data_pd(line_part_t, window=5, percentile=99):
    """
    Remove obvious outliers based on window comparison - pandas version
    Returns cleaned data in the same format: [(timestamp, value), ...]
    """
    #line_part_t =  line_part_t[2000:2100]
    if len(line_part_t) < window:
        return line_part_t

    x, y = zip(*line_part_t)
    
    # Create pandas Series and calculate rolling median
    series = pd.Series(y)
    medians = series.rolling(window=window, center=True, min_periods=1).median()
    
    # Calculate deviations
    deviations = np.abs(series - medians)
    
    largest_deviations = deviations.nlargest(10)    
    #print(largest_deviations)    
    
    # Create mask for good points
    deviation_threshold = np.percentile(deviations, percentile)
    good_points = deviations <= deviation_threshold
    
    # Convert back to numpy arrays for filtering
    x = np.array(x)
    y = np.array(y)
    
    # Return filtered data
    return list(zip(x[good_points], y[good_points]))

def add_boundary_points(line_part_t, time_zone):
    """
    Add boundary points (00:00:00 and 23:59:59) to a time series list.
    
    Args:
        line_part_t: List of tuples (timestamp, value)
        time_zone: String representing the timezone (e.g., "America/Los_Angeles")
    
    Returns:
        List of tuples with added boundary points
    """
    if not line_part_t:
        return line_part_t

    tz = pytz.timezone(time_zone)
    
    # Get the date from the first point
    first_dt = datetime.datetime.fromtimestamp(line_part_t[0][0], tz)
    date = first_dt.date()
    
    # Create datetime objects for start and end of the day
    start_dt = tz.localize(datetime.datetime.combine(date, datetime.datetime.min.time()))
    end_dt = tz.localize(datetime.datetime.combine(date, datetime.datetime.max.time()))
    
    # Convert to timestamps
    start_ts = start_dt.timestamp()
    end_ts = end_dt.timestamp()
    
    result = list(line_part_t)
    
    # Handle start point (00:00:00)
    first_point_dt = datetime.datetime.fromtimestamp(line_part_t[0][0], tz)
    time_diff = first_point_dt - start_dt

    start_value = line_part_t[0][1]
        
    # Add start point at the beginning
    result.insert(0, (start_ts, start_value))
    
    # Handle end point (23:59:59)
    last_point_dt = datetime.datetime.fromtimestamp(line_part_t[-1][0], tz)
    end_value = line_part_t[-1][1]    
    # Add end point
    result.append((end_ts, end_value))
    
    return result

class well_api:
    def on_get_healthz(self, req, resp):
        resp.status = HTTP_200
        resp.content_type = falcon.MEDIA_TEXT
        resp.text = "OK"
        
    def on_get(self, req, resp, path=""):        
        
        logger.debug(f"Sent variables: {req.params}")
        
        if path == "" or path == "/":
            
            #resp.media = package_response("Log-Out", HTTP_401)
            #return            
            
            blob_data = read_file("well_portal.html")
            resp.content_type = "text/html"
            resp.text = blob_data
            return
        elif path == "favicon.ico":
            favicon_path = "favicon.ico" 
            if os.path.isfile(favicon_path):
                resp.content_type = 'image/x-icon'
                resp.data = read_file(favicon_path, type_ = "BIN")
                resp.status = falcon.HTTP_200
            else:
                resp.status = falcon.HTTP_404

        name = req.params.get('name')
        
        if name == "deployment_add":
            token = req.params.get('token')
            user_info = verify_token(token)            
        
            if user_info == None:
                resp.media = package_response("Log-Out", HTTP_401)
                return
            user_id = req.params.get('user_id')
            blob_data = read_file("edit_deployment.html")
            caretaker = {'deployment_id': 0, 'beneficiary_id': user_id, 'caretaker_id': user_id, 'owner_id': user_id, 'installer_id': user_id, 'user_id': 0, 'role_ids': '2', 'access_to_deployments': '', 'email': '', 'user_name': '', 'first_name': '', 'last_name': '', 'address_street': '', 'address_city': '', 'address_zip': '', 'address_state': '', 'address_country': '', 'phone_number': '', 'picture': '/', 'key': ''}
            blob_data = FillFields(blob_data, caretaker, 1)   
            resp.content_type = "text/html"
            resp.text = blob_data
            return    
        
        elif name == "devices_list":
            user_id = req.params.get('user_id')
            token = req.params.get('token')
            user_info = verify_token(token)
    
            if user_info == None:
                resp.media = package_response("Log-Out", HTTP_401)
                return

            privileges = GetPriviledgesOnly(user_id)            
            
            first_s = req.params.get('first')
            last_s = req.params.get('last')

            try:
                first = int(first_s)
            except ValueError:
                first = 0
                
            try:
                last = int(last_s)
            except ValueError:
                last = 1000000                    
            blob_data = read_file("my_devices.html")
            
            devices = GetVisibleDevices(privileges)
            users = GetUsersFromDeployments(privileges)
            blob_data = UpdateDevicesTable(blob_data, devices, users)
            blob_data = UpdateDeploymentsSelector(blob_data, users)
            resp.content_type = "text/html"
            resp.text = blob_data
            return
        
        elif name == "deployment_edit":
            deployment_id = req.params.get('deployment_id')
            token = req.params.get('token')
            user_info = verify_token(token)
    
            if user_info == None:
                resp.media = package_response("Log-Out", HTTP_401)
                return
            
            blob_data = read_file("edit_deployment.html")
            
            deployment = DeploymentDetails(deployment_id)
            #blob_data = blob_data.decode("utf-8")
            blob_data = FillFields(blob_data, deployment, 1)                    
            resp.content_type = "text/html"
            resp.text = blob_data
            return                    

        elif name == "caretaker_add":        
            token = req.params.get('token')
            user_info = verify_token(token)
        
            if user_info == None:
                resp.media = package_response("Log-Out", HTTP_401)
                return
            
            blob_data = read_file("edit_caretaker.html")
            caretaker = {'user_id': 0, 'role_ids': '2', 'access_to_deployments': '', 'email': '', 'user_name': '', 'first_name': '', 'last_name': '', 'address_street': '', 'address_city': '', 'address_zip': '', 'address_state': '', 'address_country': '', 'phone_number': '', 'picture': '/', 'key': ''}
            blob_data = FillFields(blob_data, caretaker, 1)   
            resp.content_type = "text/html"
            resp.text = blob_data
            return                    
        elif name == "caretaker_edit":
            user_id = req.params.get('user_id')
            token = req.params.get('token')
            user_info = verify_token(token)

            if user_info == None:
                resp.media = package_response("Log-Out", HTTP_401)
                return
            
            blob_data = read_file("edit_caretaker.html")
            
            caretaker = UserDetails(user_id)
            #blob_data = blob_data.decode("utf-8")
            blob_data = FillFields(blob_data, caretaker, 1)                    
            resp.content_type = "text/html"
            resp.text = blob_data
            return            
        
        elif name == "beneficiary_edit":
            user_id = req.params.get('user_id')
            token = req.params.get('token')
            user_info = verify_token(token)

            if user_info == None:
                resp.media = package_response("Log-Out", HTTP_401)
                return
            
            blob_data = read_file("edit_beneficiary.html")
            
            beneficiary = UserDetails(user_id)
            #blob_data = blob_data.decode("utf-8")
            blob_data = FillFields(blob_data, beneficiary, 1)                    
            resp.content_type = "text/html"
            resp.text = blob_data
            return
        elif name == "beneficiary_add":        
            token = req.params.get('token')
            user_info = verify_token(token)
    
            if user_info == None:
                resp.media = package_response("Log-Out", HTTP_401)
                return
            user_id = req.params.get('user_id')
            blob_data = read_file("edit_beneficiary.html")
            beneficiary = {'user_id': 0, 'role_ids': '1', 'access_to_deployments': '', 'email': '', 'user_name': '', 'first_name': '', 'last_name': '', 'address_street': '', 'address_city': '', 'address_zip': '', 'address_state': '', 'address_country': '', 'phone_number': '', 'picture': '/', 'key': ''}
            blob_data = FillFields(blob_data, beneficiary, 1)   
            resp.content_type = "text/html"
            resp.text = blob_data
            return            
        elif name == "get_image_file":
            #image represents day in local time
            token = req.params.get('token')
            user_info = verify_token(token)
    
            if user_info == None:
                resp.media = package_response("Log-Out", HTTP_401)
                return
            
            deployment_id = req.params.get('deployment_id')
            time_zone_s = GetTimeZoneOfDeployment(deployment_id)
            ddate = req.params.get("date")
            ddate = ddate.replace("_","-")
            group_by = req.params.get("group_by")
            timee = StringToEpoch(ddate, time_zone_s)
            force_recreate = req.params.get("re_create") == "true"
            radar_part = req.params.get("radar_part")
            map_type = req.params.get("map_type")
            
            filename = f"/{deployment_id}/{ddate}_{group_by}_{radar_part}_{map_type}_dayly_image.png"
                        
            #print(check_file_exists(filename))
            if not force_recreate:
                file_exists, time_modified_utc = check_file_exists(filename)
                if file_exists:
                    time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                    time_modified_date = time_modified_local.date()
                    file_date = MapFileToDate(filename)
                    if time_modified_date <= file_date:
                        force_recreate = True
                else:    
                    force_recreate = True
                    
            #ddate is in Local Time
            timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
            #time that describes new devices in deployment_history is in UTC therefore timee is in UTC
            devices_list =  GetProximityList(deployment_id, timee)

            st = time.time()
            vocs_scaled = {}
            #file_date is in Local time, so we are comparing that and current Local (to install) Date 
            if force_recreate:
                st = time.time()
                vocs_scaled = {}
                stored = CreateMapFast(filename, devices_list, ddate, map_type, vocs_scaled, time_zone_s, radar_part, group_by) #"[bit] 1=same sensors together, 2=same device together, 4=1 der, 8=2 der
                if stored != True:
                    AddToLog("Map not created")
                    #logger.warning("Map not created")
                    resp.media = package_response("Map not created", HTTP_401)
                    return
                else:
                    AddToLog("Map created")
                    #print(time.time() - st)

            #lets read and send image from blob
            image_bytes, content_type = GetBlob(filename)
            if debug:
                AddToLog("@1") 
                resp.media = package_response(f'Log: {debug_string}', HTTP_200)
            else: 
                if image_bytes is None:
                    raise falcon.HTTPNotFound(
                        title='Image not found',
                        description=f'Image {filename} could not be found or retrieved'
                    )
                    
                # Set response content type and body
                resp.content_type = content_type
                resp.data = image_bytes
                resp.status = falcon.HTTP_200            
                return
            
        resp.media = package_response("Use POST method for this endpoint", HTTP_400)
        
    def on_post(self, req, resp, path=""):
        
        logger.debug(f"Request method: {req.method}")
        logger.debug(f"Request path: {req.path}")
        logger.debug(f"Request query string: {req.query_string}")
        logger.debug(f"Request headers: {req.headers}")
        logger.debug(f"Request content type: {req.content_type}")

        # Use Falcon's built-in parsing
        if req.content_type == falcon.MEDIA_URLENCODED:
            form_data = req.media
        elif req.content_type == falcon.MEDIA_JSON:
            form_data = req.media
        else:
            form_data = {}
            
        logger.debug(f"Parsed form data: {form_data}")

         # Extract parameters

        
        try:
            function = form_data.get('function')

            if function != "credentials":
                token = form_data.get('token')
                user_info = verify_token(token)

                if user_info == None:
                    resp.media = package_response("Log-Out", HTTP_401)
                    return
                username = user_info['username'] 
                with get_db_connection() as db_conn:
                    privileges = GetPriviledgesOnly(username)
                    
            if function == "credentials":
                
                username = form_data.get('user_name')
                if not username:
                    resp.media = package_response("Missing 'user_name' parameter", HTTP_400)
                    return
                
                ps = form_data.get('ps')

                if username == MASTER_ADMIN and ps == MASTER_PS:
                    access_token = generate_token(username)
                    privileges, user_id = ValidUser(username, ps)                    
                    privileges = "-1"
                else:
                    #lets check for real
                    privileges, user_id = ValidUser(username, ps)
                    if privileges == "0":
                        access_token = 0
                        privileges = 0
                    else:
                        access_token = generate_token(username)

                token_payload = {'access_token': access_token, 'privileges': privileges, 'user_id': user_id}
                resp.media = package_response(token_payload)
                resp.status = falcon.HTTP_200                                
                return 

            elif function == "request_single_slice":
                deployment_id = form_data.get('deployment_id')
                time_zone_s = GetTimeZoneOfDeployment(deployment_id)
                selected_date = form_data.get('date')
                devices_list = form_data.get('devices_list')
                #devices_list = '[267,560,"?",null,"64B70888F6F0"]'
                #devices_list = '[[267,560,"?",null,"64B70888F6F0"],[268,561,"?",null,"64B70888F6F1"]]'
                sensor_list_loc = [form_data.get('sensor_list')]
                is_nested, device_details = check_and_parse(devices_list)
                if not is_nested:
                    device_ids_list = [device_details[1]]
                    well_ids_list = [device_details[0]]
                else:
                    device_ids_list = list(map(lambda x: x[1], device_details))
                    well_ids_list =list(map(lambda x: x[0], device_details))

                data_type = form_data.get('data_type')
                epoch_from_utc, epoch_to_utc = GetLocalTimeEpochsForDate(selected_date, time_zone_s) #>= #< 

                #epoch_to = '1730592010' #smal sample to test
                radar_part = form_data.get('radar_part')
                well_id = well_ids_list[0]
                all_slices = {}
                for device_id in device_ids_list:
                    
                    sensor_data = {}
                    for sensor in sensor_list_loc:
                        st = time.time()
                        line_part = ReadSensor(device_id, sensor, epoch_from_utc, epoch_to_utc, data_type, radar_part)
                        window =  sensor_legal_values[sensor][2]
                        #print("@1", time.time() - st)
                        #first = 3300
                        #last = 3400
                        #line_part =  line_part[first:last]
                        line_part_t = []
                        #st = time.time()
                        #line_part_t = [tuple(x[:2]) for x in line_part]
                        #print(time.time() - st)
                        #st = time.time()
                        #line_part_t = list({(dt.timestamp(), value) for dt, value in line_part})
                        #print(time.time() - st)
                        
                        line_part_t = [(x[0].timestamp(), x[1]) for x in line_part]                        
                        st = time.time()
                        cleaned_values_t = clean_data_pd(line_part_t, window=window, percentile=99)
                        cleaned_values = add_boundary_points(cleaned_values_t, time_zone_s)
                        #print("@2", time.time() - st)
                        
                        #Lets add point in minute 0 and minute 1439
                        
                        #st = time.time()
                        #cleaned_values = clean_data_fast(line_part_t, window=5, threshold=2.0)
                        #print("@3", time.time() - st)
                        
                        sensor_data[sensor] = cleaned_values
                    all_slices[device_id] = sensor_data
                
                    dataa = {}
                    dataa['Function'] = "single_slicedata"
                    dataa['MACs_list'] = device_ids_list
                    dataa['all_slices'] = all_slices
                    dataa['well_id'] = well_id
                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200                
                #return
            elif function == "get_deployment":
                blob_data = read_file("deployment.html")
                deployment_id = form_data.get('deployment_id') 
                #lets update "Deployments" select
                users = GetUsersFromDeployments(privileges)
                blob_data = UpdateDeploymentsSelector(blob_data, users, False, deployment_id)
                
                resp.content_type = "text/html"
                resp.text = blob_data
                return
            elif function == "request_deployment_map_new":
                deployment_id = form_data.get('deployment_id')
                maps_dates, positions_list = GetDeploymentDatesBoth(deployment_id)
                datee = form_data.get('date')
                if maps_dates != []:
            
                    if datee == "2022-4-2": #that one is default in HTML so disregard
                        datee = maps_dates[0]
            
                    locations_desc_map = {}
                    for details in positions_list:
                        well_id = details[0]
                        location = details[2]
                        if details[3] != None:
                            location = location +" "+ details[3]
                        MAC = details[4]
                        locations_desc_map[well_id] = location
            
                    dataa = {}
                    dataa['Function'] = "deployments_maps_report"
                    dataa['proximity'] = positions_list
                    maps_dates.sort(reverse = True)
                    dataa['maps_dates'] = maps_dates
                    dataa['device_count'] = len(positions_list)
            
                    #MACs_list = GetMACsListSimple(positions_list)
                    #MACs_map = {}
            
                    #for details in positions_list:
                    #    id = details[0]
                    #    MAC = details[3]
                    #    MACs_map[id] = MAC
                    #for i in range(len(MACs_list)):
                    #  MACs_map[devices_list[i]] = MACs_list[i][0]
            
                    id = positions_list[0][0]
                    #dataa['MACs_map'] = MACs_map
                    dataa['locations_desc_map'] = locations_desc_map              
                    #proximity_list = proximity.split(",")
                    if id < 200:                  
                        checkmarks_string = 'T><input checked type="checkbox" id="t-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'H><input type="checkbox" id="h-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'P><input type="checkbox" id="p-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'C><input type="checkbox" id="c-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'V><input type="checkbox" id="v-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'L><input type="checkbox" id="l-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'R><input type="checkbox" id="r-check" value="0" onchange="UpdateSelections();"/><br>'
                    else: #>200 = ["Temperature", "Humidity", "Pressure", "Light", "Radar", "VOC"]
            
                        checkmarks_string = 'T><input checked type="checkbox" id="t-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'H><input type="checkbox" id="h-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'P><input type="checkbox" id="p-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'L><input type="checkbox" id="l-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'R><input type="checkbox" id="r-check" value="0" onchange="UpdateSelections();"/>\n'
            
                        checkmarks_string = checkmarks_string + 'S0><input type="checkbox" id="v0-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S1><input type="checkbox" id="v1-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S2><input type="checkbox" id="v2-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S3><input type="checkbox" id="v3-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S4><input type="checkbox" id="v4-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S5><input type="checkbox" id="v5-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S6><input type="checkbox" id="v6-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S7><input type="checkbox" id="v7-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S8><input type="checkbox" id="v8-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S9><input type="checkbox" id="v9-check" value="0" onchange="UpdateSelections();"/><br>'
                        
                    checked_or_not = " checked"
                    
                    for index in range(len(positions_list)):
                        details = positions_list[index]
                        device_id = details[0]
                        location = details[2]
                        if details[3] != None:
                            location = location + " "+details[2]                    
            
                        checkmarks_string = checkmarks_string + str(device_id) + '><input'+checked_or_not+' type="checkbox" id="device_check'+str(index)+'" value="0"  title="'+location+'" onchange="UpdateSelections();" />\n'
                        checked_or_not = ''
            
                    dataa['checkmarks'] = checkmarks_string
                    resp.media = package_response(dataa)
                    resp.status = falcon.HTTP_200
                    
            elif function == "request_proximity":
                deployment = form_data.get('deployment_id')
                timee = form_data.get('time')
                #timee = StringToEpoch(datee)
                #print(deployment, timee)
                well_ids, device_ids = GetProximityList(deployment, timee)
                #print(proximity)
                dataa = {}
                dataa['Function'] = "proximity_report"
                if len(well_ids) > 0:                
                    dataa['proximity'] = well_ids
                else:
                    dataa['proximity'] = []
                
                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200                      
            elif function == "request_devices":
                deployment_id = form_data.get('deployment_id')
                group_id = form_data.get('group_id')
                location = form_data.get('location')
                is_fresh = form_data.get('is_fresh')
                matching_devices = GetMatchingDevices(privileges, group_id, deployment_id, location)
                dataa = {}
                dataa['Function'] = "devices_report"
                if len(matching_devices) > 0:                
                    dataa['devices'] = matching_devices
                else:
                    dataa['devices'] = []
                
                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200                  
                
                
            elif function == "get_raw_data":
                container = GetReference("/MAC")
                MAC = req_dict["MAC"][0]
                sensor = req_dict["sensor"][0]
                if "part" in req_dict:                        
                    part = req_dict["part"][0]
                else:
                    part = ""
                from_time = req_dict["from_time"][0]
                to_time = req_dict["to_time"][0]
                timezone_str = req_dict["tzone"][0]
                AddToLog("get_raw_data:" + str(MAC) +","+ str(sensor) + "," + str(from_time) + "," + str(to_time) + "," + part+ "," + timezone_str)
                #raw_data = GetRawSensorData(container, MAC, sensor, from_time, to_time, timezone_str)
                raw_data = GetRawSensorDataFromBlobStorage(MAC, sensor, part, from_time, to_time, timezone_str)
                data_payload = {'raw_data': raw_data}
                resp.media = package_response(data_payload)
                resp.status = falcon.HTTP_200                
                return
                            
            elif function == "get_candle_data":
                container = GetReference("/MAC")
                MAC = req_dict["MAC"][0]
                sensor = req_dict["sensor"][0]
                from_time = req_dict["from_time"][0]
                to_time = req_dict["to_time"][0]
                part = req_dict["part"][0]
                tzone = req_dict["tzone"][0]
                AddToLog(str(req_dict))
                candle_data = GetCandleSensorData(container, MAC, sensor, from_time, to_time, part, tzone)
                data_payload = {'candle_data': candle_data}
                resp.media = package_response(data_payload)
                resp.status = falcon.HTTP_200     
                return

            elif function == "deployment_form":
                editing_deployment_id = form_data.get('editing_deployment_id')
                
                ok = StoreDeployment2DB(form_data, editing_deployment_id)
                if ok == 1:
                    payload = {'ok': ok}
                    resp.media = package_response(payload)
                    resp.status = falcon.HTTP_200                         
                    return
                else:
                    payload = {'ok': ok, 'error': debug_string}
                    resp.media = package_response(payload)
                    resp.status = falcon.HTTP_200     
                    return
            elif function == "deployment_delete":                                
                ok = DeleteRecordFromDB(form_data)

                payload = {'ok': ok}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200     
                return  

            elif function == "deployments_list":
                result_list = []
                first_s = form_data.get('first')
                last_s = form_data.get('last')

                try:
                    first = int(first_s)
                except ValueError:
                    first = 0
                    
                try:
                    last = int(last_s)
                except ValueError:
                    last = 1000000                    
                
                user_id = form_data.get('user_id')
                all_deployments = ListDeployments(privileges, user_id)
                    
                cnt = 0

                for deployment in all_deployments:
                    cnt += 1
                    if cnt >= first:
                        caretaker_min_object = {"deployment_id": deployment['deployment_id'], "email": user_id_2_user[deployment['beneficiary_id']][3], "first_name": user_id_2_user[deployment['beneficiary_id']][5], "last_name": user_id_2_user[deployment['beneficiary_id']][6]}
                        result_list.append(caretaker_min_object)
                    if cnt > last:
                        break

                payload = {'result_list': result_list}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200     
                return                

            elif function == "caretaker_form":
                editing_user_id = form_data.get('editing_user_id')
                email = form_data.get('email')
                
                if "@" in email:
                    ok = StoreCaretaker2DB(form_data, editing_user_id)
                    if ok == 1:
                        payload = {'ok': ok}
                        resp.media = package_response(payload)
                        resp.status = falcon.HTTP_200                         
                        return
                    else:
                        payload = {'ok': ok, 'error': debug_string}
                        resp.media = package_response(payload)
                        resp.status = falcon.HTTP_200     
                        return
                else:
                    resp.media = package_response("Missing or illegal 'email' parameter", HTTP_400)
                    return
                
            elif function == "caretaker_delete":
                if privileges == "-1":                    
                    ok = DeleteRecordFromDB(form_data)
                else:
                    ok = 0
                AddToLog(ok)
                payload = {'ok': ok}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200     
                return

            elif function == "caretakers_list":
                result_list = []
                first_s = form_data.get('first')
                last_s = form_data.get('last')

                try:
                    first = int(first_s)
                except ValueError:
                    first = 0
                    
                try:
                    last = int(last_s)
                except ValueError:
                    last = 1000000                    
                
                if privileges == "-1":
                    all_caretakers = ListCaretakers()
                        
                    cnt = 0
    
                    for caretaker in all_caretakers:
                        cnt += 1
                        if cnt >= first:
                            caretaker_min_object = {"user_id": caretaker[0], "email": caretaker[3], "first_name": caretaker[5], "last_name": caretaker[6]}
                            result_list.append(caretaker_min_object)
                        if cnt > last:
                            break

                payload = {'result_list': result_list}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200     
                return                
            
            elif function == "beneficiary_form":
                editing_user_id = form_data.get('editing_user_id')
                email = form_data.get('email')
                if "@" in email:
                    ok = StoreBeneficiary2DB(form_data, editing_user_id)
                    if ok == 1:
                        payload = {'ok': ok}
                        resp.media = package_response(payload)
                        resp.status = falcon.HTTP_200     
                        return                        
                    else:
                        payload = {'ok': ok, 'error': debug_string}
                        resp.media = package_response(payload)
                        resp.status = falcon.HTTP_200     
                        return
                else:
                    payload = {'ok': ok}
                    resp.media = package_response(payload)
                    resp.status = falcon.HTTP_200     
                    return  
                
            elif function == "beneficiary_delete":
                
                
                ok = DeleteRecordFromDB(form_data)

                payload = {'ok': ok}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200     
                return  

            elif function == "beneficiaries_list":
                result_list = []
                first_s = form_data.get('first')
                last_s = form_data.get('last')

                try:
                    first = int(first_s)
                except ValueError:
                    first = 0
                    
                try:
                    last = int(last_s)
                except ValueError:
                    last = 1000000                    
                
                user_id = form_data.get('user_id')
                all_beneficiaries = ListBeneficiaries(privileges, user_id)
                    
                cnt = 0

                for beneficiary in all_beneficiaries:
                    cnt += 1
                    if cnt >= first:
                        beneficiary_min_object = {"user_id": beneficiary[0], "email": beneficiary[3], "first_name": beneficiary[5], "last_name": beneficiary[6]}
                        result_list.append(beneficiary_min_object)
                    if cnt > last:
                        break

                payload = {'result_list': result_list}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200     
                return                
            elif function == "dashboard_list":
                AddToLog(req_dict)
                caretaker = req_dict["user"][0] 
                all_beneficiaries = ListBeneficiariesOfCaretaker(caretaker)                
                AddToLog(all_beneficiaries)
                result_list = []

                for beneficiary in all_beneficiaries:
                    
                    details = GetDetails(beneficiary)
                    #"[[104, 'Living Room', '', '64B708890988'], [106, 'Bathroom', '', '64B708890298'], [101, 'Bedroom', '', '64B7088902B8'], [130, 'Dining Room', '', '64B7088905C8'], [107, 'Bathroom', 'Malo ', '4C75259783E0'], [102, 'Kitchen', '', '308398C72E58'], [141, 'Office', '', '64B70888FB50']]"
                    #lets determine where person is now
                    devices_string = details["devices"]
                    devices_list = ast.literal_eval(devices_string)
                    timezone_str = details["tzone"]
                    timezone_object = pytz.timezone(timezone_str)

                    latest_presence = ""
                    latest_MAC = ""
                    temperature_offset = 16
                    bathroom_presence = ""
                    kitchen_presence = ""
                    
                    for device in devices_list:
                        if len(device) > 3:
                            MAC = device[3]
                            this_presence, temp_offset = GetLastPresent(MAC)                                                            
                            
                            if latest_presence == "":
                                
                                if this_presence != "":
                                    latest_presence = this_presence
                                    latest_MAC = MAC

                            if "bathroom" in device[1].lower():
                                bathroom_presence = this_presence

                            if "kitchen" in device[1].lower():
                                kitchen_presence = this_presence


                            if latest_presence != "":

                                AddToLog("@3 ")                                
                                AddToLog(this_presence)
                                AddToLog(latest_presence)
                                #this_time = datetime.datetime.strptime(this_presence, "%Y-%m-%d %H:%M:%S.%f", tzinfo=timezone_object)
                                this_time = strptime_tz(this_presence, timezone_str, "%Y-%m-%d %H:%M:%S.%f")
                                #latest_time = datetime.datetime.strptime(latest_presence, "%Y-%m-%d %H:%M:%S.%f", tzinfo=timezone_object)
                                latest_time = strptime_tz(latest_presence, timezone_str, "%Y-%m-%d %H:%M:%S.%f")
                                AddToLog(this_time)
                                AddToLog(latest_time)
                                if this_time > latest_time:
                                    AddToLog("@4")
                                    latest_presence = this_presence
                                    latest_MAC = MAC
                                    temperature_offset = temp_offset
                                    AddToLog("latest_presence = ")
                                    AddToLog(MAC+"  "+latest_presence)
                    
                    #lets get latest readings for latest_MAC
                    if latest_MAC != "":
                        device_dict = GetSensorsReads(latest_MAC)
                    
                        #get latest read of temperature, VOC, CO2
                        details["temperature"] = device_dict["temperature"]["last_data"]
                        #apply temp offsett
                        details["temperature"]["value"] = details["temperature"]["value"] - temperature_offset
                        details["smell"] = {"timestamp": 1722198473.330856,"value": "coffee","intensity": 54} #device_dict["last_data"]
                        details["sleep"] = 7 + random.uniform(-2, 2) #todo, determine real value
                        details["last_bathroom"] = bathroom_presence
                        details["last_kitchen"] = kitchen_presence
                    
                    details["HomeIcon"] = "air_green"    
                    details["BathIcon"] = "bath_yellow"    
                    details["BedIcon"] = "bed_red"    
                    details["FoodIcon"] = "food_green"    
                    
                    user = details
                    result_list.append(user)

                payload = {'result_list': result_list}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200     
                AddToLog(payload)
                return

            else:
                AddToLog("Error: function not recognized!") 
                payload = {'ok': ok, 'error': debug_string}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200     
                return
            
        except Exception as e:
            AddToLog(traceback.format_exc())    
            resp.media = package_response(f"Error: {str(e)}  {traceback.format_exc()}", HTTP_500)


class RootResource:
    def on_get(self, req, resp):
        resp.media = {
            "message": "Hello from OpenFaaS Serverless Web Server!",
            "method": "GET"
        }

    def on_post(self, req, resp):
        data = json.load(req.bounded_stream)
        resp.media = {
            "message": "Received POST request",
            "data": data
        }

class CatchAllResource:
    def on_get(self, req, resp, path):
        resp.media = {
            "message": f"Path: /{path}",
            "method": "GET"
        }

    def on_post(self, req, resp, path):
        resp.media = {
            "message": f"Path: /{path}",
            "method": "POST"
        }

app = falcon.App()

# New routes for well_api
app.add_route('/function/well-api', well_api())
app.add_route('/function/well-api/{path}', well_api())
app.add_route('/api/well_api', well_api())
app.add_route('/api/well_api/{path}', well_api())
app.add_route('/healthz', well_api(), suffix='healthz')

# Keep the original routes for backward compatibility
app.add_route('/', well_api())
app.add_route('/{path}', well_api())

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    with make_server('', 8000, app) as httpd:
        logger.debug('Serving on port 8000...')
        httpd.serve_forever()