import os
import json
import logging
from logging.handlers import RotatingFileHandler
from typing import List

from fastapi import FastAPI, HTTPException, Path, Query, File, UploadFile, Security, status
from fastapi.responses import FileResponse
from fastapi.security import APIKeyHeader, APIKeyQuery
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

import sqlite3

# ==================== Logging Configuration ====================

def get_log_level(level_str: str) -> int:
    """
    Convert a string log level to a logging level.
    """
    level_str = level_str.upper()
    if level_str == "INFO":
        return logging.INFO
    elif level_str == "DEBUG":
        return logging.DEBUG
    elif level_str == "ERROR":
        return logging.ERROR
    elif level_str == "CRITICAL":
        return logging.CRITICAL
    elif level_str == "FATAL":
        return logging.FATAL
    else:
        return logging.INFO  # Default level

# Get log level from environment variable or default to INFO
log_level_str = os.getenv('LOG_LEVEL', 'INFO')
log_level = get_log_level(log_level_str)

# Create logger
logger = logging.getLogger("config_distributer")
logger.setLevel(log_level)
logger.propagate = False  # Prevent double logging

# Formatter
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')

# File handler with rotation
file_handler = RotatingFileHandler("app.log", maxBytes=5*1024*1024, backupCount=2)
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)

# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(log_level)
console_handler.setFormatter(formatter)

# Add handlers to logger
if not logger.handlers:
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)

# ==================== Security Setup ====================

api_key_query = APIKeyQuery(name="api-key", auto_error=False)
api_key_header = APIKeyHeader(name="x-api-key", auto_error=False)
API_KEYS = os.getenv('API_KEYS', 'a,b,c').split(",")

assets_dir = os.getenv('ASSETS_DIR', './assets')

def get_api_key(
    api_key_query: str = Security(api_key_query),
    api_key_header: str = Security(api_key_header),
) -> str:
    """
    Retrieve and validate the API key from query or header.
    """
    if api_key_query in API_KEYS:
        logger.debug(f"API key '{api_key_query}' found in query parameters.")
        return api_key_query
    if api_key_header in API_KEYS:
        logger.debug(f"API key '{api_key_header}' found in headers.")
        return api_key_header
    logger.warning("Invalid or missing API Key.")
    raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid or missing API Key",
    )

# ==================== FastAPI Application ====================

app = FastAPI()

# CORS configuration
origins = ["*"]  # Allow all origins; modify as needed for security

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,  # Specify allowed origins
    allow_credentials=True,
    allow_methods=["*"],    # Specify allowed HTTP methods
    allow_headers=["*"],    # Specify allowed headers
)

# ==================== Database Setup ====================

DATABASE = 'workers.db'
conn = sqlite3.connect(DATABASE, check_same_thread=False)
cursor = conn.cursor()

def create_worker_table(worker_name: str):
    """
    Create a table for the worker if it doesn't exist.
    """
    try:
        cursor.execute(f'''
            CREATE TABLE IF NOT EXISTS {worker_name} (
                key TEXT PRIMARY KEY,
                value TEXT
            )
        ''')
        conn.commit()
        logger.debug(f"Table '{worker_name}' ensured in database.")
    except Exception as e:
        logger.critical(f"Failed to create table '{worker_name}': {e}")
        raise

def convert_dict_values_to_int(dic: dict) -> dict:
    """
    Convert dictionary values that are digit strings to integers.
    """
    for key, value in dic.items():
        if isinstance(value, str) and value.isdigit():
            dic[key] = int(value)
    logger.debug(f"Converted dictionary values to int where applicable: {dic}")
    return dic

class WorkerConfig(BaseModel):
    key: str
    value: str

# ==================== API Endpoints ====================

@app.put("/workers/{worker_name}")
async def update_worker_config(
    worker_names: List[str],
    # Uncomment the next line to enforce API key security
    # api_key: str = Security(get_api_key),
    configs: List[WorkerConfig]
):
    """
    Update configurations for specified workers.
    """
    logger.info(f"Received request to update configs for workers: {worker_names}")
    try:
        for worker_name in worker_names:
            create_worker_table(worker_name)
            for config in configs:
                cursor.execute(
                    f"INSERT OR REPLACE INTO {worker_name} (key, value) VALUES (?, ?)",
                    (config.key, config.value)
                )
            conn.commit()
            logger.debug(f"Configurations updated for worker '{worker_name}'.")
        return {"message": f"Configurations updated for workers: {', '.join(worker_names)}"}
    except Exception as e:
        logger.error(f"Error updating worker configurations: {e}")
        raise HTTPException(status_code=500, detail="Internal Server Error")

@app.post("/initialize")
async def initialize_db_from_json(
    api_key: str = Security(get_api_key),
    file: UploadFile = File(...)
):
    """
    Initialize the database from a JSON file.
    """
    logger.info("Received request to initialize database from JSON.")
    if file.content_type != "application/json":
        logger.warning("Invalid file type received for initialization.")
        raise HTTPException(status_code=400, detail="Invalid file type. JSON expected.")
    
    try:
        data = await file.read()
        data = json.loads(data)
        logger.debug("JSON file successfully read and parsed.")
    except json.JSONDecodeError:
        logger.error("Failed to decode JSON file.")
        raise HTTPException(status_code=400, detail="Invalid JSON file.")
    except Exception as e:
        logger.error(f"Unexpected error reading file: {e}")
        raise HTTPException(status_code=500, detail="Internal Server Error.")
    
    try:
        for worker_name, config in data.items():
            create_worker_table(worker_name)
            for key, value in config.items():
                cursor.execute(
                    f"INSERT OR REPLACE INTO {worker_name} (key, value) VALUES (?, ?)",
                    (key, str(value))
                )
            conn.commit()
            logger.debug(f"Worker '{worker_name}' initialized with config.")
        logger.info("Database successfully initialized from JSON file.")
        return {"message": "Database initialized from JSON file."}
    except Exception as e:
        logger.error(f"Error initializing database from JSON: {e}")
        raise HTTPException(status_code=500, detail="Internal Server Error")

@app.get("/workers/{worker_name}")
async def get_worker_config(
    api_key: str = Security(get_api_key),
    worker_name: str = Path(..., title="The name of the worker"),
    key: str = Query(None, title="Config Key", description="Optional configuration key"),
    convert_to_int: bool = Query(False, title="Convert to int", description="Convert string numbers to integers")
):
    """
    Retrieve configurations for a specific worker.
    """
    logger.info(f"Received request to get config for worker '{worker_name}'.")
    create_worker_table(worker_name)
    try:
        if key:
            cursor.execute(f"SELECT value FROM {worker_name} WHERE key=?", (key,))
            result = cursor.fetchone()
            if not result:
                logger.warning(f"Configuration key '{key}' not found for worker '{worker_name}'.")
                raise HTTPException(status_code=404, detail="Configuration key not found")
            value = result[0]
            if convert_to_int and value.isdigit():
                value = int(value)
                logger.debug(f"Converted value '{value}' to int for key '{key}'.")
            return {key: value}
        else:
            cursor.execute(f"SELECT key, value FROM {worker_name}")
            config = cursor.fetchall()
            if not config:
                logger.warning(f"No configurations found for worker '{worker_name}'.")
                raise HTTPException(status_code=404, detail="Configuration not found")
            converted_dict = {param: value for param, value in config}
            if convert_to_int:
                converted_dict = convert_dict_values_to_int(converted_dict)
            logger.debug(f"Retrieved configurations for worker '{worker_name}': {converted_dict}")
            return converted_dict
    except HTTPException as he:
        raise he
    except Exception as e:
        logger.error(f"Error retrieving configurations for worker '{worker_name}': {e}")
        raise HTTPException(status_code=500, detail="Internal Server Error")

@app.get("/assets/{image_name}")
async def get_image(
    image_name: str = Path(..., title="The name of the image", description="Name of the image file to retrieve")
):
    """
    Retrieve an image by its name from the assets directory.
    """
    logger.info(f"Received request to get image '{image_name}'.")
    image_path = os.path.join(assets_dir, image_name)

    if not os.path.isfile(image_path):
        logger.warning(f"Image '{image_name}' not found in assets directory.")
        raise HTTPException(status_code=404, detail="Image not found")
    
    logger.debug(f"Serving image '{image_name}' from '{image_path}'.")
    return FileResponse(image_path)

@app.post("/assets/upload")
async def upload_file(file: UploadFile = File(...)):
    """
    Upload an image and save it in the assets directory.
    """
    logger.info(f"Received request to upload image '{file.filename}'.")
    try:
        # Ensure assets directory exists
        os.makedirs(assets_dir, exist_ok=True)
        file_path = os.path.join(assets_dir, file.filename)

        with open(file_path, "wb") as upload_file:
            content = await file.read()
            upload_file.write(content)
            logger.debug(f"File '{file.filename}' uploaded successfully to '{file_path}'.")
        
        return {"message": f"File '{file.filename}' successfully uploaded."}
    except Exception as e:
        logger.error(f"Failed to upload file '{file.filename}': {e}")
        raise HTTPException(status_code=500, detail="Internal Server Error")

# ==================== Application Entry Point ====================

if __name__ == "__main__":
    import uvicorn
    try:
        logger.info("Starting FastAPI application.")
        uvicorn.run(app, host="0.0.0.0", port=8000)
    except Exception as e:
        logger.critical(f"Application failed to start: {e}")
        raise

# ==================== Unit Tests ====================

# ==================== Unit Tests ====================

import unittest
from fastapi.testclient import TestClient

class TestConfigDistributer(unittest.TestCase):
    """
    Unit tests for the config_distributer FastAPI application.
    """

    @classmethod
    def setUpClass(cls):
        """
        Set up the TestClient and initialize the database.
        """
        cls.client = TestClient(app)
        # Initialize database with sample data
        cls.sample_json = {
            "test_worker": {
                "test_key": "test_value",
                "number_key": "123"
            }
        }
        # Create a temporary JSON file
        cls.temp_json_path = "temp_config.json"
        with open(cls.temp_json_path, 'w') as f:
            json.dump(cls.sample_json, f)

    @classmethod
    def tearDownClass(cls):
        """
        Clean up after tests.
        """
        # Remove temporary JSON file
        if os.path.exists(cls.temp_json_path):
            os.remove(cls.temp_json_path)
        # Close the database connection
        conn.close()
        # Remove test database
        if os.path.exists(DATABASE):
            os.remove(DATABASE)
        # Remove uploaded test images
        test_image = os.path.join(assets_dir, "test_image.png")
        test_text = os.path.join(assets_dir, "test.txt")
        another_test_image = os.path.join(assets_dir, "another_test_image.png")
        if os.path.exists(test_image):
            os.remove(test_image)
        if os.path.exists(test_text):
            os.remove(test_text)
        if os.path.exists(another_test_image):
            os.remove(another_test_image)

    def setUp(self):
        """
        Reset the database before each test.
        """
        # Ensure the database is in a known state
        create_worker_table("test_worker")
        cursor.execute(
            "INSERT OR REPLACE INTO test_worker (key, value) VALUES (?, ?)",
            ("test_key", "test_value")
        )
        cursor.execute(
            "INSERT OR REPLACE INTO test_worker (key, value) VALUES (?, ?)",
            ("number_key", "123")
        )
        conn.commit()

    def tearDown(self):
        """
        Clean up the database after each test.
        """
        cursor.execute(f"DELETE FROM test_worker")
        conn.commit()

    def test_initialize_db_from_json(self):
        """
        Test the /initialize endpoint with a valid JSON file.
        """
        with open(self.temp_json_path, 'rb') as f:
            response = self.client.post(
                "/initialize",
                headers={"x-api-key": "a"},
                files={"file": ("temp_config.json", f, "application/json")}
            )
        self.assertEqual(response.status_code, 200)
        self.assertIn("Database initialized from JSON file", response.json().get("message", ""))

        # Verify that data from JSON is present
        response = self.client.get(
            "/workers/test_worker",
            headers={"x-api-key": "a"}
        )
        expected_config = self.sample_json["test_worker"]
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), expected_config)

    def test_get_worker_config_all(self):
        """
        Test retrieving all configurations for a worker.
        """
        response = self.client.get(
            "/workers/test_worker",
            headers={"x-api-key": "a"}
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), self.sample_json["test_worker"])

    def test_get_worker_config_specific_key(self):
        """
        Test retrieving a specific configuration key for a worker.
        """
        response = self.client.get(
            "/workers/test_worker",
            headers={"x-api-key": "a"},
            params={"key": "test_key"}
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {"test_key": "test_value"})

    def test_get_worker_config_convert_to_int(self):
        """
        Test retrieving a configuration with integer conversion.
        """
        response = self.client.get(
            "/workers/test_worker",
            headers={"x-api-key": "a"},
            params={"key": "number_key", "convert_to_int": "true"}
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {"number_key": 123})

    def test_update_worker_config(self):
        """
        Test updating worker configurations.
        """
        update_data = {
            "configs": [
                {"key": "new_key", "value": "new_value"},
                {"key": "number_key", "value": "456"}
            ]
        }
        response = self.client.put(
            "/workers/test_worker",
            headers={"x-api-key": "a"},
            json=update_data
        )
        self.assertEqual(response.status_code, 200)
        self.assertIn("Configurations updated for worker: test_worker", response.json().get("message", ""))
        
        # Verify update
        response = self.client.get(
            "/workers/test_worker",
            headers={"x-api-key": "a"}
        )
        expected_config = {
            "test_key": "test_value",
            "number_key": "456",
            "new_key": "new_value"
        }
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), expected_config)

    def test_get_image_not_found(self):
        """
        Test retrieving a non-existent image.
        """
        response = self.client.get(
            "/assets/non_existent_image.png"
        )
        self.assertEqual(response.status_code, 404)
        self.assertIn("Image not found", response.json().get("detail", ""))

    def test_upload_and_get_image(self):
        """
        Test uploading an image and then retrieving it.
        """
        # Upload image
        image_content = b"test_image_content"
        response = self.client.post(
            "/assets/upload",
            headers={"x-api-key": "a"},
            files={"file": ("test_image.png", image_content, "image/png")}
        )
        self.assertEqual(response.status_code, 200)
        self.assertIn("successfully uploaded", response.json().get("message", ""))
        logger.info("Image uploaded successfully.")

        # Retrieve image
        response = self.client.get("/assets/test_image.png")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.content, image_content)
        logger.info("Image retrieved successfully.")

    def test_upload_invalid_file_type(self):
        """
        Test uploading a file with an invalid file type.
        """
        response = self.client.post(
            "/assets/upload",
            headers={"x-api-key": "a"},
            files={"file": ("test.txt", b"just some text", "text/plain")}
        )
        self.assertEqual(response.status_code, 200)  # Assuming upload is allowed for any file type
        self.assertIn("successfully uploaded", response.json().get("message", ""))
        logger.info("Invalid file type uploaded successfully.")

    def test_protected_endpoint_without_api_key(self):
        """
        Test accessing a protected endpoint without providing an API key.
        """
        response = self.client.post(
            "/initialize",
            files={"file": ("temp_config.json", b"{}", "application/json")}
        )
        self.assertEqual(response.status_code, 401)
        self.assertIn("Invalid or missing API Key", response.json().get("detail", ""))
        logger.info("Access without API key correctly denied.")

    def test_protected_endpoint_with_invalid_api_key(self):
        """
        Test accessing a protected endpoint with an invalid API key.
        """
        response = self.client.post(
            "/initialize",
            headers={"x-api-key": "invalid_key"},
            files={"file": ("temp_config.json", b"{}", "application/json")}
        )
        self.assertEqual(response.status_code, 401)
        self.assertIn("Invalid or missing API Key", response.json().get("detail", ""))
        logger.info("Access with invalid API key correctly denied.")

    def test_retrieve_uploaded_image(self):
        """
        Test retrieving an image after uploading it.
        """
        # Upload image
        image_content = b"another_test_image_content"
        response = self.client.post(
            "/assets/upload",
            headers={"x-api-key": "a"},
            files={"file": ("another_test_image.png", image_content, "image/png")}
        )
        self.assertEqual(response.status_code, 200)
        self.assertIn("successfully uploaded", response.json().get("message", ""))
        logger.info("Another image uploaded successfully.")

        # Retrieve image
        response = self.client.get("/assets/another_test_image.png")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.content, image_content)
        logger.info("Another image retrieved successfully.")

    def test_initialize_db_with_invalid_json(self):
        """
        Test initializing the database with an invalid JSON file.
        """
        invalid_json_content = b"{invalid_json: true}"  # Missing quotes around key
        response = self.client.post(
            "/initialize",
            headers={"x-api-key": "a"},
            files={"file": ("invalid_config.json", invalid_json_content, "application/json")}
        )
        self.assertEqual(response.status_code, 400)
        self.assertIn("Invalid JSON file", response.json().get("detail", ""))
        logger.info("Initialization with invalid JSON correctly failed.")

if __name__ == "__main__":
    # Run unit tests
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
gives 
PS C:\Users\omarm\Downloads\fasakh> python -m unittest omar.py
2024-09-19 23:18:04,038 - INFO - Received request to get image 'non_existent_image.png'.
2024-09-19 23:18:04,038 - WARNING - Image 'non_existent_image.png' not found in assets directory.
.2024-09-19 23:18:04,052 - INFO - Received request to get config for worker 'test_worker'.
.2024-09-19 23:18:04,066 - INFO - Received request to get config for worker 'test_worker'.
.2024-09-19 23:18:04,079 - INFO - Received request to get config for worker 'test_worker'.
.2024-09-19 23:18:04,096 - INFO - Received request to initialize database from JSON.
2024-09-19 23:18:04,103 - INFO - Database successfully initialized from JSON file.
2024-09-19 23:18:04,106 - INFO - Received request to get config for worker 'test_worker'.
.2024-09-19 23:18:04,121 - INFO - Received request to initialize database from JSON.
2024-09-19 23:18:04,121 - ERROR - Failed to decode JSON file.
2024-09-19 23:18:04,122 - INFO - Initialization with invalid JSON correctly failed.
.2024-09-19 23:18:04,135 - WARNING - Invalid or missing API Key.
2024-09-19 23:18:04,136 - INFO - Access with invalid API key correctly denied.
.2024-09-19 23:18:04,148 - WARNING - Invalid or missing API Key.
2024-09-19 23:18:04,150 - INFO - Access without API key correctly denied.
.2024-09-19 23:18:04,163 - INFO - Received request to upload image 'another_test_image.png'.
2024-09-19 23:18:04,167 - INFO - Another image uploaded successfully.
2024-09-19 23:18:04,168 - INFO - Received request to get image 'another_test_image.png'.
2024-09-19 23:18:04,201 - INFO - Another image retrieved successfully.
.F2024-09-19 23:18:04,226 - INFO - Received request to upload image 'test_image.png'.
2024-09-19 23:18:04,229 - INFO - Image uploaded successfully.
2024-09-19 23:18:04,231 - INFO - Received request to get image 'test_image.png'.
2024-09-19 23:18:04,233 - INFO - Image retrieved successfully.
.2024-09-19 23:18:04,245 - INFO - Received request to upload image 'test.txt'.
2024-09-19 23:18:04,248 - INFO - Invalid file type uploaded successfully.
.
======================================================================
FAIL: test_update_worker_config (omar.TestConfigDistributer)
Test updating worker configurations.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\omarm\Downloads\fasakh\omar.py", line 448, in test_update_worker_config
    self.assertEqual(response.status_code, 200)
AssertionError: 422 != 200

----------------------------------------------------------------------
Ran 12 tests in 0.240s

FAILED (failures=1)
PS C:\Users\omarm\Downloads explani the erreur in update config why and what is these