from pathlib import Path
import json
from typing import Optional

import aiofiles
from jsonschema import validate, ValidationError
from inventory_handler.InventoryTypes import Inventory, PropertyName, Property
from inventory_handler.InventoryUtils import logger


class InventoryRepository:
    """
    Handles reading, writing, and validating inventory data.
    """

    def __init__(self, inventoryPath: Path, schemaPath: Path):
        """
        Initialize the inventory repository with paths and schema.

        :param inventoryPath: The file path to the inventory.
        :type inventoryPath: Path
        :param schemaPath: The file path to the JSON schema.
        :type schemaPath: Path
        """
        self.inventoryPath: Path = inventoryPath
        self.schema: dict = self.__readSchema(schemaPath)

    async def create(self):
        """
        Create an empty inventory file with default structure.

        This method ensures the parent directories exist before writing.
        """
        logger.info(f"{self.inventoryPath} does not exist. Creating empty inventory file.")
        self.inventoryPath.parent.mkdir(parents=True, exist_ok=True)

        async with aiofiles.open(self.inventoryPath, mode="w") as f:
            await f.write(json.dumps({"properties": {}}, indent=4))

    async def readAll(self) -> Inventory:
        """
        Asynchronously read and return inventory data from file.

        If the file does not exist, it is created automatically.

        :return: The validated inventory data.
        :rtype: Inventory
        :raises Exception: If an unexpected error occurs during file I/O or JSON decoding.
        """
        try:
            async with aiofiles.open(self.inventoryPath, mode="r") as f:
                content = await f.read()
                data = json.loads(content)
                self.__validateInventory(data)
                return data
        except FileNotFoundError:
            await self.create()
            return await self.readAll()
        except Exception as e:
            logger.exception(f"Error reading {self.inventoryPath}, error: {e}")
            raise

    async def read(self, name: PropertyName) -> Optional[Property]:
        """
        Retrieve a specific property by name from the inventory.

        :param name: The property name to look up.
        :type name: PropertyName
        :return: The matching property if found, else None.
        :rtype: Optional[Property]
        """
        inventory = await self.readAll()
        return inventory["properties"].get(name.value)

    async def write(self, name: PropertyName, payload: Property) -> bool:
        """
        Write a single property to the inventory and persist it.

        :param name: The property name to write.
        :type name: PropertyName
        :param payload: The property data to store.
        :type payload: Property
        :return: True if write succeeded.
        :rtype: bool
        """
        inventory = await self.readAll()
        inventory["properties"][name.value] = payload
        return await self.writeAll(inventory)

    async def writeAll(self, inventory: Inventory) -> bool:
        """
        Write the entire inventory dictionary to the file.

        :param inventory: The full inventory data.
        :type inventory: Inventory
        :return: True if write succeeded.
        :rtype: bool
        :raises Exception: If writing fails or validation fails.
        """
        try:
            if not self.inventoryPath.exists():
                await self.create()

            self.__validateInventory(inventory)

            async with aiofiles.open(self.inventoryPath, mode="w") as f:
                await f.write(json.dumps(inventory, indent=4))
                return True
        except Exception as e:
            logger.exception(f"Error writing to {self.inventoryPath}, error: {e}")
            raise

    def __readSchema(self, path: Path) -> dict:
        """
        Load and return the JSON schema.

        :param path: Path to the schema file.
        :type path: Path
        :return: The parsed JSON schema.
        :rtype: dict
        :raises RuntimeError: If the schema cannot be loaded.
        """
        try:
            with open(path, "r") as f:
                return json.load(f)
        except Exception as e:
            raise RuntimeError(f"Schema could not be loaded: {e}") from e

    def __validateInventory(self, inventory: Inventory) -> None:
        """
        Validate the given inventory data against the loaded JSON schema.

        :param inventory: The inventory data to validate.
        :type inventory: Inventory
        :raises ValidationError: If the inventory data does not conform to the schema.
        """
        try:
            validate(instance=inventory, schema=self.schema)
        except ValidationError as e:
            raise ValidationError(f"Invalid data: {e.message}") from e
