import os
import json

from gumband_python_sdk.logging_handler import get_local_logger
from gumband_python_sdk.network_handler import NetworkHandler
from gumband_python_sdk.service_name import ServiceName

SERVICE = ServiceName.CONTENT

local_logger = get_local_logger()


class ContentService:
    """
    ContentService

    A service class for managing content synchronization between the local exhibit and Gumband.
    It handles downloading files, comparing file metadata, and synchronizing the local file system with remote content.
    This class is intended as a private class. Please use the Gumband service instead.

    Attributes:
        ready_for_sync (bool): Indicates whether the service is ready to sync remote content files.
        sync_in_progress (bool): Prevents duplicate sync attempts while a file sync is ongoing.
    """

    ready_for_sync = False
    sync_in_progress = False

    def __init__(self, exhibit_id: int, network_handler: NetworkHandler, content_location: str = None, skip_organization_file_sync: bool = True):
        """
        Initializes the ContentService.

        Args:
            exhibit_id (int): The ID of the exhibit for which the content is being managed.
            network_handler (NetworkHandler): An instance of the network handler for API communication.
            content_location (str, optional): The local directory where the content files are stored.
                If not provided, this service is essentially inert.
        """
        self.exhibit_id = exhibit_id
        self.network_handler = network_handler
        self.content_location = content_location
        self.skip_organization_file_sync = skip_organization_file_sync
        if self.content_location != None:
            # Create content location directory if it does not exist
            os.makedirs(content_location, exist_ok=True)
            self.ready_for_sync = True

    async def _download_file(self, file):
        """
        Downloads a file from the remote Gumband server.

        Args:
            file (str): The name of the file to download.
        """
        try:
            endpoint = f"/files?exhibitId={self.exhibit_id}&fileName={file}"

            response = await self.network_handler.get(service=SERVICE, endpoint=endpoint, stream=True)

            if response.status_code != 200:
                local_logger.error(f"Error: {response.status_code}, Response: {await response.text()}")
                return

            with open(f"{self.content_location}/{file}", "wb") as file:
                for chunk in response.iter_content(chunk_size=8192):
                    file.write(chunk)

        except Exception as e:
            local_logger.error(f"Error occurred during file download: {e}")

    async def _get_remote_file_list(self):
        """
        Retrieves the list of files from Gumband. This list will match the files under the files tab
        for this exhibit.

        Returns:
            response (Response): The server's response containing the list of files.
        """
        endpoint = f"/files/ls?exhibitId={self.exhibit_id}"
        response = await self.network_handler.get(service=SERVICE, endpoint=endpoint)
        return response

    def _get_local_file_list(self):
        """
        Retrieves the list of files from the local content metadata file.

        Returns:
            dict: The local file list as stored in the metadata file.
        """
        files = {"files": []}  # Default empty file structure

        try:
            # Read metadata file
            metadata_path = f"{self.content_location}/.metadata.json"
            with open(metadata_path, "r", encoding="utf-8") as files_metadata:
                files = json.load(files_metadata)

            indexes_to_remove = []

            # Iterate through the file list
            for index, file in enumerate(files["files"]):
                try:
                    os.stat(f"{self.content_location}/{file['file']}")
                    local_logger.debug(f"File found: {file['file']}")
                except FileNotFoundError:
                    local_logger.debug(f"File not found in local file list: {file['file']}")
                    indexes_to_remove.append(index)

            # Remove missing files from the list in reverse order
            if indexes_to_remove:
                for index in sorted(indexes_to_remove, reverse=True):
                    removed_file = files["files"].pop(index)
                    local_logger.debug(f"File removed from local content metadata file: {removed_file}")

                # Write updated metadata back to file
                with open(metadata_path, "w", encoding="utf-8") as metadata_file:
                    json.dump(files, metadata_file, indent=4)

        except Exception as e:
            local_logger.error(f"Error reading local file list: {e}")

        return files

    async def _download_missing_files(self, files, local_metadata):
        """
        Downloads the files that are missing from the local content.

        Args:
            files (list): A list of files to download.
            local_metadata (dict): The metadata containing the current local file list.
        """
        for i, file in enumerate(files):
            local_logger.info(f"Downloading file {i + 1} of {len(files)} - {int(file['size']) / 1000} kb")

            try:
                await self._download_file(file["file"])
                local_metadata["files"].append(file)

                with open(os.path.join(self.content_location, ".metadata.json"), "w") as f:
                    json.dump(local_metadata, f)
                local_logger.info("Wrote local content metadata file")

            except Exception as e:
                error_message = f"Download file error: {str(e)}"
                local_logger.error(error_message)

    async def sync(self):
        """
        Syncs the local and remote file systems by comparing files and downloading any missing or updated files.

        This method will:
            - Skip sync if the content location is not specified, the service is not ready, or if sync is already in progress.
            - Update the metadata file if files are added, removed, or updated.
            - Download missing or updated files.
        """
        if not self.content_location:
            local_logger.info("No content directory specified. Skipping file sync.")
            return

        if not self.ready_for_sync:
            local_logger.warning("Content class not ready for sync - skipping")
            return

        if self.sync_in_progress:
            local_logger.warning("Sync already in progress - skipping duplicate call")
            return

        self.sync_in_progress = True
        local_logger.info("Content sync started")

        local_files = self._get_local_file_list()
        remote_files = await self._get_remote_file_list()
        need_to_download = []
        meta_data_updated = False

        for i, file in enumerate(remote_files["files"]):
            found = False
            is_org_file = False

            if file["file"].startswith("[ORG] "):
                file["file"] = file["file"].replace("[ORG] ", "")
                is_org_file = True

            local_logger.info(f"Remote file: {file['file']} ({i + 1} of {len(remote_files['files'])})")

            if self.skip_organization_file_sync and is_org_file:
                local_logger.info("Skipping organization file download due to SDK constructor param")
                continue

            for j in range(len(local_files["files"])):
                local_file = local_files["files"][j]
                if (
                    file["hash"] == local_file["hash"]
                    and file["file"] == local_file["file"]
                    and file["lastUpdated"] == local_file["lastUpdated"]
                    and file["size"] == local_file["size"]
                ):
                    local_logger.info("Found local copy, skipping in download queue")
                    found = True
                elif file["file"] == local_file["file"] and (file["lastUpdated"] != local_file["lastUpdated"] or file["size"] != local_file["size"]):
                    local_logger.info(
                        "Found existing file with same name but different size and/or updated timestamp. Pruning metadata to replace file."
                    )
                    removed_entry = local_files["files"].pop(j)
                    j -= 1  # Adjust index after removal
                    meta_data_updated = True
                    local_logger.debug(f"Removed file {removed_entry['file']} from metadata to be replaced.")

            if not found:
                local_logger.info("Did not find current local copy, adding to download queue")
                need_to_download.append(file)

        if meta_data_updated:
            with open(os.path.join(self.content_location, ".metadata.json"), "w") as f:
                json.dump(local_files, f)

        if not need_to_download:
            local_logger.info("No new files to download")
        else:
            await self._download_missing_files(need_to_download, local_files)

        self.sync_in_progress = False
        local_logger.info("Content sync complete")
