"""Common Handler Module.

This module provides common functionality and utilities for automation workflows in the JL platform.
It includes the CommonManager class which acts as a central hub for shared operations such as
execution time management, blob storage operations, SMS notifications, report generation,
and various utility functions used across different automation workflows.

Key Features:
    - Execution time tracking and management
    - Azure Blob Storage operations
    - SMS notifications via Twilio
    - Report generation and caching
    - Redis integration for caching
    - SharePoint integration
    - Database operations
    - Timezone handling utilities

Example:
    Basic usage within an automation:
        common = CommonManager(sharepoint, api, db, automation_details, config)
        last_exec_time = await common.get_last_exec_time()
        await common.send_sms("+1234567890", "Hello from automation!")
"""

from dateutil.parser import parse
from twilio.rest import Client
import os
import uuid
import pandas as pd
import datetime as dt
import pytz
import logging
import json
from io import BytesIO
from typing import Any, Tuple, Dict, Optional, Set
from .sql_handler import SqlManager
from azure.storage.blob.aio import BlobServiceClient
from .mainsubsysapi_handler import MainSubSysApiManager, RequestType
from .sharepoint_handler import SharePointManager
from .appconfig_handler import Configurations
from dataclasses import dataclass
from io import StringIO
import asyncio
from .common_utility import deprecated, ReportStatus, ReportRequestError, ReportStatusError, WINDOWS_TO_IANA
import platform
from datetime import datetime
from zoneinfo import ZoneInfo
import pytz
import aiohttp
import redis.asyncio as redis
from datetime import timedelta
from requests.exceptions import RequestException

@dataclass
class ExecutionResult:
    """Data class to hold automation execution results and statistics.
    
    This class encapsulates the results of an automation execution, including
    performance metrics, operation counts, and optional custom information.
    
    Attributes:
        execution_time (str): ISO format timestamp of the execution.
        read_count (int): Number of read operations performed.
        create_count (int): Number of create operations performed.
        update_count (int): Number of update operations performed.
        delete_count (int): Number of delete operations performed.
        failure_count (int): Number of failed operations.
        custom_message (Optional[str]): Custom message or notes about the execution.
        azure_blob_storage (Optional[str]): Reference to associated blob storage data.
    """
    execution_time: str
    read_count: int = 0
    create_count: int = 0
    update_count: int = 0
    delete_count: int = 0
    failure_count: int = 0
    no_op_count: int = 0
    custom_message: Optional[str] = None
    azure_blob_storage: Optional[str] = None
    
    def __post_init__(self):
        """Validate required fields after initialization.
        
        Raises:
            ValueError: If execution_time is not provided.
        """
        if not self.execution_time:
            raise ValueError("execution_time is required")


class CommonManager:
    """Central manager class for common automation functionality.
    
    This class provides a comprehensive set of utilities and services commonly used
    across different automation workflows. It integrates with various external services
    including Azure Blob Storage, Redis, Twilio, SharePoint, and the main subsystem API.
    
    Attributes:
        sharepoint (SharePointManager): SharePoint operations manager.
        api (MainSubSysApiManager): Main subsystem API manager.
        db (SqlManager): Database operations manager.
        code (str): Automation code identifier.
        redis (Optional[redis.Redis]): Redis client for caching operations.
        tenantid (str): Tenant identifier.
        deploymentid (str): Deployment unique identifier.
        description (str): Automation description.
        twilio (Dict[str, str]): Twilio configuration settings.
        sms_client (Client): Twilio SMS client.
        config (Configurations): Configuration manager instance.
        env (Optional[str]): Application environment.
        reports (Dict[str, asyncio.Event]): Report generation event tracking.
        report_data (Dict[str, pd.DataFrame]): Cached report data.
        blob_configurations (Dict[str, str]): Blob storage configuration.
        blob_connection_string (str): Azure Blob Storage connection string.
        blob_container_name (str): Azure Blob Storage container name.
    """

    def __init__(self, sharepoint: SharePointManager, api: MainSubSysApiManager, db: SqlManager, automation, config: Configurations) -> None:
        """Initialize the CommonManager with required dependencies and services.
        
        Args:
            sharepoint (SharePointManager): Configured SharePoint manager instance.
            api (MainSubSysApiManager): Configured API manager instance.
            db (SqlManager): Configured database manager instance.
            automation (Any): Automation details object containing metadata.
            config (Configurations): Configuration manager with service settings.
            
        Raises:
            Exception: If there are issues initializing Redis or other services.
        """
        self.sharepoint = sharepoint
        self.api = api
        self.db = db
        self.code = automation.code
        
        # Initialize Redis client with error handling
        try:
            self.redis = redis.from_url(config.Redis)
        except Exception as e:
            logging.error(f"Failed to initialize Redis client: {e}")
            self.redis = None
            
        self.tenantid = automation.tenant_id
        self.deploymentid = automation.deployment_id
        self.description = automation.description

        # Initialize Twilio SMS client
        self.twilio = json.loads(config.twilio)
        self.sms_client = Client(self.twilio["api_key_sid"], self.twilio["api_key_secret"], self.twilio["account_sid"])

        self.config = config
        self.env = os.getenv("APPLICATION_ENVIRONMENT")
        
        # Report management
        self.reports: Dict[str, asyncio.Event] = {}
        self.report_data: Dict[str, pd.DataFrame] = {}
        self._background_tasks: Set[asyncio.Task] = set()

        # Azure Blob Storage configuration
        self.blob_configurations = json.loads(self.config.blob_storage_configurations)
        self.blob_connection_string = self.blob_configurations["connection_string"]
        self.blob_container_name = self.blob_configurations["container_name"]

    async def get_last_exec_time_legacy(self):
        """Get the last execution time from execution history (legacy method).
        
        This method retrieves the most recent execution time from the ExecutionHistory
        table for the current deployment. If no execution history exists, it returns
        the current timestamp.
        
        Returns:
            str: ISO formatted timestamp of the last execution or current time.
            
        Raises:
            Exception: If there are database query errors.
            
        Note:
            This is a legacy method maintained for backward compatibility.
            New implementations should use get_last_exec_time() instead.
        """
        try:
            query = """
            SELECT TOP 1 CAST(ExecutionTime AS VARCHAR) AS ExecutionTime FROM [AutomationNew].[ExecutionHistory]
            WHERE DeployedAutomationId = ?
            ORDER BY ExecutionTime DESC
            """
            header = "ExecutionTime"
            last_run = await self.db.get_async(query, header, (self.deploymentid))
            
            if last_run.empty:
                execution_time = dt.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
            else:
                execution_time = last_run["ExecutionTime"].iloc[0]
                
            return parse(execution_time).strftime("%Y-%m-%dT%H:%M:%SZ")
        except Exception as e:
            logging.error(f"An error occurred in get_last_exec_time_legacy: {e}")
            raise e
    
    async def get_last_exec_time(self):
        """Get the last execution time from deployed automation table.
        
        This method retrieves the LastExecutionTime directly from the DeployedAutomation
        table, which provides better performance than querying the ExecutionHistory table.
        
        Returns:
            str: ISO formatted timestamp of the last execution.
            
        Raises:
            Exception: If there are database query errors.
        """
        try:
            query = """
            SELECT CAST(LastExecutionTime AS VARCHAR) AS ExecutionTime FROM [AutomationNew].[DeployedAutomation]
            WHERE UniqueId = ?
            """
            header = "ExecutionTime"
            last_run = await self.db.get_async(query, header, (self.deploymentid))
            if last_run.empty or pd.isna(last_run["ExecutionTime"].iloc[0]):
                execution_time = await self.get_last_exec_time_legacy()
                return execution_time
            else:
                execution_time = last_run["ExecutionTime"].iloc[0]
            return parse(execution_time).strftime("%Y-%m-%dT%H:%M:%SZ")
        except Exception as e:
            logging.error(f"An error occurred in get_last_exec_time: {e}")
            raise e

    async def update_execution_history_async(self, result: ExecutionResult) -> None:
        """
        Updates execution history using ExecutionResult dataclass.
        
        Args:
            result: ExecutionResult containing execution details
        Raises:
            ValueError: If execution_time is None
            Exception: For DB or unexpected errors
        """
        try:
            if not result.execution_time:
                raise ValueError("Missing required parameter: 'execution_time'")

            if result.failure_count == 0:
                status_id = 1
            elif result.failure_count < result.read_count:
                status_id = 2
            else:
                status_id = 3

            response_parts = [
                f"A total of {result.read_count} jobs retrieved from Joblogic."
            ]
            if result.create_count > 0:
                response_parts.append(f"{result.create_count} records created.")
            if result.update_count > 0:
                response_parts.append(f"{result.update_count} records updated.")
            if result.delete_count > 0:
                response_parts.append(f"{result.delete_count} records deleted.")
            response_parts.append(f"There were {result.failure_count} failures.")
            response_msg = " ".join(response_parts)

            query = """
            INSERT INTO [AutomationNew].[ExecutionHistory]
            (UniqueId, TenantId, ExecutionTime, StatusId, ResponseMessage, ReadCount,
            CreateCount, UpdateCount, DeleteCount, FailureCount, AzureBlobStorage,
            CreatedAt, UpdatedAt, isDeleted, DeployedAutomationId)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """

            current_time = dt.datetime.now()
            query_params = (
                str(uuid.uuid4()),
                self.tenantid,
                result.execution_time,
                status_id,
                response_msg,
                result.read_count,
                result.create_count,
                result.update_count,
                result.delete_count,
                result.failure_count,
                result.azure_blob_storage or "",
                current_time,
                current_time,
                0,
                self.deploymentid,
            )

            await self.db.execute_async(query, query_params)
            await self.update_last_exec_deployedautomation(result.execution_time)
            
        except Exception as e:
            logging.error(f"Error updating execution time: {str(e)}", exc_info=True)
            raise

    @deprecated("Use update_execution_history_async instead, which accepts ExecutionResult dataclass.")
    async def update_last_exec_time_async(self, params: Dict[str, Any]) -> None:
        """
        Inserts or updates execution time and status for the automation run.
        :param params: Dictionary with execution details (e.g., ExecutionTime, ReadCount, etc.)
        """
        try:
            defaults = {
                "ExecutionTime": None,
                "ReadCount": 0,
                "CreateCount": 0,
                "UpdateCount": 0,
                "DeleteCount": 0,
                "FailureCount": 0,
                "AzureBlobStorage": "",
            }
            params = {**defaults, **params}

            if params["ExecutionTime"] is None:
                raise ValueError("Missing required parameter: 'ExecutionTime'")

            if params["FailureCount"] == 0:
                status_id = 1  # Success
            elif params["FailureCount"] < params["ReadCount"]:
                status_id = 2  # Partial success
            else:
                status_id = 3  # Failure

            params["StatusId"] = status_id

            response_msg_parts = [
                f"A total of {params['ReadCount']} jobs retrieved from Joblogic."
            ]
            if params["CreateCount"] > 0:
                response_msg_parts.append(f"{params['CreateCount']} records created.")
            if params["UpdateCount"] > 0:
                response_msg_parts.append(f"{params['UpdateCount']} records updated.")
            if params["DeleteCount"] > 0:
                response_msg_parts.append(f"{params['DeleteCount']} records deleted.")
            response_msg_parts.append(f"There were {params['FailureCount']} failures.")
            params["ResponseMsg"] = " ".join(response_msg_parts)

            query = """
            INSERT INTO [AutomationNew].[ExecutionHistory]
            (UniqueId, TenantId, ExecutionTime, StatusId, ResponseMessage, ReadCount,
             CreateCount, UpdateCount, DeleteCount, FailureCount, AzureBlobStorage,
             CreatedAt, UpdatedAt, isDeleted, DeployedAutomationId)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """

            query_params = (
                str(uuid.uuid4()),
                self.tenantid,
                params["ExecutionTime"],
                params["StatusId"],
                params["ResponseMsg"],
                params["ReadCount"],
                params["CreateCount"],
                params["UpdateCount"],
                params["DeleteCount"],
                params["FailureCount"],
                params["AzureBlobStorage"],
                dt.datetime.now(),
                dt.datetime.now(),
                0,  # isDeleted
                self.deploymentid,
            )

            await self.db.execute_async(query, query_params)
            await self.update_last_exec_deployedautomation(params["ExecutionTime"])
        except Exception as e:
            logging.error(f"An error occurred in update_last_exec_time: {e}")
            raise e
    
    async def update_last_exec_deployedautomation(self, last_exec_timestamp) -> None:
        try:
            query = f"""
            UPDATE [AutomationNew].[DeployedAutomation]
            SET LastExecutionTime = ?,
            UpdatedAT = ?
            WHERE UniqueId = ?; """
            await self.db.execute_async(query, (last_exec_timestamp,dt.datetime.now(),self.deploymentid))

        except Exception as e:
            logging.error(f"An error occurred in update_last_exec_deployedautomation: {e}")
            raise 

    async def save_data_async(self, data: Any) -> Tuple[bool, str]:
        """
        Asynchronously saves data to Azure Blob Storage.

        :param data: The data to be saved, typically a list of dict or a DataFrame.
        :return: Tuple (success, message/url).
        :raises Exception: For unexpected I/O or network errors.
        """
        try:
            sas_url = self.config.StorageConnectionString
            if not sas_url:
                raise ValueError("SAS URL is missing or empty.")

            tenant_id = self.tenantid
            auto_id = self.deploymentid
            file_name = f"{auto_id} - {dt.datetime.now().strftime('%Y-%m-%d %H_%M_%S')}"

            async with BlobServiceClient(account_url=sas_url) as blob_service_client:
                container_client = blob_service_client.get_container_client(tenant_id)
                data = data.replace(r'[^\x20-\x7E]', '', regex=True)
                
                df = pd.DataFrame(data)
                excel_buffer = BytesIO()
                df.to_excel(excel_buffer, index=False, engine="openpyxl")
                excel_buffer.seek(0)

                full_blob_name = os.path.join(auto_id, file_name).replace("\\", "/")
                blob_client = container_client.get_blob_client(full_blob_name)
                await blob_client.upload_blob(excel_buffer, overwrite=True)

                return True, blob_client.url
        except ValueError as ve:
            logging.error(f"Configuration error: {ve}")
            return False, str(ve)
        except Exception as e:
            logging.error(f"Failed to save data: {e}")
            return False, str(e)

    async def upload_file_stream_to_blob_storage(self, file_stream, file_path):
        """
        Asynchronously uploads a file stream to Azure Blob Storage.

        :param file_stream: The file stream to be uploaded.
        :param file_path: The path in the blob storage where the file will be stored.
        :return: The result of the upload operation.
        :raises ValueError: If the SAS URL is missing or empty.
        :raises Exception: For any unexpected errors during the upload process.
        """

        try:
            sas_url = self.config.StorageConnectionString
            if not sas_url:
                raise ValueError("SAS URL is missing or empty.")

            async with BlobServiceClient(account_url=sas_url) as blob_service_client:
                container_client = blob_service_client.get_container_client(
                    self.tenantid
                )
                file_path = file_path.replace("\\", "/")
                blob_client = container_client.get_blob_client(file_path)
                result = await blob_client.upload_blob(
                    file_stream, blob_type="BlockBlob", overwrite=True
                )

                return result
        except Exception as e:  
            logging.error(f"An error occurred on uploading file stream to blob: {str(e)}")
            raise

    async def download_blob_content(self, file_path):
        try:
            sas_url = self.config.StorageConnectionString
            if not sas_url:
                raise ValueError("SAS URL is missing or empty.")

            async with BlobServiceClient(account_url=sas_url) as blob_service_client:
                container_client = blob_service_client.get_container_client(
                    self.tenantid
                )
                file_path = file_path.replace("\\", "/")
                blob_client = container_client.get_blob_client(file_path)
                downloaded_blob = await blob_client.download_blob()

                return downloaded_blob

        except Exception as e:
            logging.error(f"An error occurred on downloading blob content: {str(e)}")
            raise

    @staticmethod
    async def get_time_in_utc():
        try:
            local_time = dt.datetime.now()
            utc_time = local_time.astimezone(pytz.utc)
            return utc_time.strftime("%Y-%m-%d %H:%M:%S")
        except Exception as e:
            logging.error(f"An error occurred: {str(e)}")
            raise

    async def fetch_all_reports_async(self) -> pd.DataFrame:
        """
        Fetch all reports asynchronously for the given automation code.

        :return: Dictionary containing report data.
        :raises Exception: For unexpected I/O or network errors.
        """
        try:
            query = """
                SELECT 
                    ADS.ReportId AS [ReportId], 
                    ADS.Description AS [ReportName]
                FROM 
                    AutomationNew.Automation AS A
                    INNER JOIN AutomationNew.AutomationDataSource AS ADS ON ADS.AutomationId = A.Id
                WHERE A.Code = ? AND ADS.IsDeleted = 0 AND A.IsDeleted = 0
            """
            return await self.db.get_async(query, "ReportId, ReportName", (self.code))
        except Exception as e:
            logging.error(f"An error occurred while fetching data for code {self.code}: {e}")
            raise

    def get_current_year_month_day_file_path_string(self, file_name, extension=None, include_hours=False):
        """
        Generate a file name by concatenating the given file name with the current year, month, and day.
        Optionally include the current hour. If the extension is None, it will not be appended.

        Args:
            file_name (str): The base name of the file.
            extension (str): The file extension (e.g., 'xlsx', 'csv'). If None, the extension will not be appended.
            include_hours (bool): If True, includes the current hour in the file name. Defaults to False.

        Returns:
            str: The generated file name with the format 'file_name_YYYY_MM_DD.ext' or 'file_name_YYYY_MM_DD_HH.ext' 
            if include_hours is True and extension is provided. If extension is None, it will return without the extension.

        Example:
            >>> get_current_year_month_day_file_path_string("report", "xlsx")
            'report_2024_07_17.xlsx'

            >>> get_current_year_month_day_file_path_string("report", None, include_hours=True)
            'report_2024_07_17_14'
        """
        try:
            # Get current date and time
            current_datetime = dt.datetime.now()

            # Pick day, month, and year from current date
            execution_day = current_datetime.day
            execution_month = current_datetime.month
            execution_year = current_datetime.year

            # Construct a formatted string representing the year, month, and day
            year_month_day_str = (
                f"{execution_year}_{execution_month:02d}_{execution_day:02d}"
            )

            # If include_hours is True, add the hour to the formatted string
            if include_hours:
                execution_hour = current_datetime.hour
                year_month_day_str += f"_{execution_hour:02d}"

            # Concatenate the file name with the date string
            full_file_name = f"{file_name}_{year_month_day_str}"

            # Only append the extension if it's provided (not None)
            if extension:
                full_file_name += f".{extension}"

            return full_file_name

        except Exception as e:
            logging.error(f"An error occurred while generating file path: {e}")
            raise e

    def send_sms(self, to_number, message_body, sender="Joblogic", ):
        try:
            message = self.sms_client.messages.create(
                messaging_service_sid=self.twilio["messaging_service_sid"],
                from_=sender,
                body=message_body,
                to=to_number,
            )
            return message
        except Exception as e:
            logging.error(f"An error occurred while sending SMS: {e}")
            raise e

    def get_sms_details(self, sid):
        try:
            message = self.sms_client.messages(sid).fetch()
            return message
        except Exception as e:
            logging.error(f"An error occurred while fetching SMS details: {e}")
            raise e

    def localize_time(self, utc_time: datetime, time_zone_str: str) -> datetime:
        """
        Convert UTC datetime to a target timezone.
        On Windows, map known Windows timezones to IANA. 
        On other systems, assume time_zone_str is IANA already.
        """
        if utc_time.tzinfo is None:
            utc_time = utc_time.replace(tzinfo=pytz.utc)
        elif utc_time.tzinfo != pytz.utc:
            utc_time = utc_time.astimezone(pytz.utc)

        try:
            iana_timezone = WINDOWS_TO_IANA.get(time_zone_str, time_zone_str)
            return utc_time.astimezone(ZoneInfo(iana_timezone))
        except Exception as e:
            raise ValueError(f"Failed to localize to '{time_zone_str}': {e}")
    
    async def search_report_msg(self, report_id: int, tenantid :str = None) -> pd.DataFrame:
        try:
            payload = {"id": report_id}
            response = await self.api.base_method_async(api_url="reports/download-report-data", method=RequestType.POST, payload=payload, tenant_id = tenantid)
            if response.status == 200:
                data = await response.json()
                headers = data["data"]["headers"]
                rows = data["data"]["rows"]
                df = pd.DataFrame(rows, columns=headers)
                return df
            else:
                raise RequestException(f"Failed to fetch report data. Status code: {response.status}")
        except Exception as e:
            logging.error(f"An error occurred: {str(e)}")
            raise
        
    async def get_reliable_report_data(self, report_id: int, pass_to_account: bool = False, retry: bool = False, retry_count: int = 3, duplicate_protection: bool = False,
                                       last_execution_time: Optional[datetime] = None, tenantid :str = None) -> Optional[pd.DataFrame]:
        
        tenantid = tenantid or self.tenantid

        if self.env != "Live":
            df = await self.search_report_msg(report_id, tenantid)
            csv_buffer = StringIO()
            df.to_csv(csv_buffer, index=False)
            csv_buffer.seek(0)
            return pd.read_csv(csv_buffer)

        POLLING_INTERVAL = 5
        MAX_POLLING_TIME = 600
        RETRY_DELAY = 10
        REDIS_KEY_PREFIX = "report_export:"

        async def check_report_status(export_id: str) -> tuple[ReportStatus, Optional[str]]:
            response = await self.api.base_method_async(f'reports/report-result?exportId={export_id}', RequestType.GET, tenant_id = tenantid)
            if response.status == 400:
                raise ReportStatusError(f"Invalid report status request for export_id: {export_id}")
            if response.status != 200:
                raise ReportStatusError(f"Unexpected status code: {response.status}")
            result = await response.json()
            return ReportStatus(result['status']), result.get('url')

        async def download_csv(url: str) -> pd.DataFrame:
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as response:
                    if response.status == 200:
                        data = await response.text()
                        return pd.read_csv(StringIO(data))
                    else:
                        raise ValueError(f"Failed to download CSV from {url}")
        
        async def update_last_exec_time_async(report_id, last_exec_time) -> None:
            try:
                payload = { "id": report_id, "lastExecDateTime": f"{last_exec_time}" }
                response = await self.api.base_method_async(f'reports/update-last-exec-datetime', RequestType.POST, payload=payload, tenant_id = tenantid)
                if response.status != 200:
                    raise ValueError(f"Failed to update last execution time for report_id: {report_id}")
                logging.info(f"Successfully updated last execution time for report_id: {report_id}")
            except Exception as e:
                logging.error(f"Error updating execution time: {str(e)}", exc_info=True)
                raise

        try:
            if last_execution_time is not None:
                await update_last_exec_time_async(report_id, last_execution_time)
                
            payload = {
                'reportId': report_id,
                'markasPassedToAccounts': pass_to_account
            }
            response = await self.api.base_method_async('reports/schedule-report', RequestType.POST, payload=payload, tenant_id = tenantid)

            if response.status == 400:
                raise ReportRequestError(f"Invalid report request for report_id: {report_id}")
            if response.status != 202:
                raise ReportRequestError(f"Unexpected status code: {response.status}")

            response_json = await response.json()
            export_id = response_json['exportId']

            if duplicate_protection and self.redis:
                try:
                    redis_key = f"{REDIS_KEY_PREFIX}{export_id}"
                    exists = await self.redis.exists(redis_key)
                    
                    if exists:
                        logging.info(f"Export ID {export_id} already processed within time window")
                        return None

                    expiry_minutes = int(self.config.ExportExpiry or 60)
                    await self.redis.setex(
                        redis_key,
                        timedelta(minutes=expiry_minutes),
                        "1"
                    )
                    logging.debug(f"Added export_id {export_id} to Redis with {expiry_minutes} minutes expiry")
                except redis.RedisError as e:
                    logging.warning(f"Redis operation failed, proceeding with report fetch: {str(e)}")
                except Exception as e:
                    logging.warning(f"Unexpected Redis error, proceeding with report fetch: {str(e)}")


            start_time = asyncio.get_event_loop().time()
            while True:
                if asyncio.get_event_loop().time() - start_time > MAX_POLLING_TIME:
                    raise TimeoutError("Report generation timed out")

                status, url = await check_report_status(export_id)

                if status == ReportStatus.SUCCEEDED:
                    return await download_csv(url)

                elif status == ReportStatus.FAILED:
                    if retry and retry_count > 0:
                        logging.warning(f"Report generation failed, retrying... ({retry_count} retries left)")
                        await asyncio.sleep(RETRY_DELAY)
                        return await self.get_reliable_report_data(
                            report_id, 
                            pass_to_account, 
                            retry, 
                            retry_count - 1,
                            duplicate_protection
                        )
                    else:
                        raise RequestException("Report generation failed after all retries")
                
                elif status in (ReportStatus.NOT_STARTED, ReportStatus.RUNNING):
                    await asyncio.sleep(POLLING_INTERVAL)

                else:
                    raise ValueError(f"Unknown report status: {status}")

        except Exception as e:
            logging.error(f"An error occurred while fetching report data: {e}")
            raise

    async def schedule_report(self, report_name: str, report_id: int, 
                            pass_to_account: bool = False, 
                            duplicate_protection: bool = False) -> None:
        """Schedule a report and track it in background"""
        if report_name in self.reports:
            raise ValueError(f"Report {report_name} already scheduled")

        self.reports[report_name] = asyncio.Event()
        task = asyncio.create_task(
            self._fetch_report(
                report_name, 
                report_id, 
                pass_to_account,
                duplicate_protection
            )
        )
        self._background_tasks.add(task)
        task.add_done_callback(self._background_tasks.discard)


    async def wait_for_report(self, report_name: str, timeout: Optional[float] = None) -> pd.DataFrame:
        """Wait for a report to complete and return its data"""
        if report_name not in self.reports:
            raise ValueError(f"Report {report_name} not scheduled")

        await asyncio.wait_for(self.reports[report_name].wait(), timeout)
        return self.report_data[report_name]

    async def _fetch_report(self, report_name: str, report_id: int, 
                          pass_to_account: bool, 
                          duplicate_protection: bool) -> None:
        """Background task to fetch report data"""
        try:
            df = await self.get_reliable_report_data(
                report_id=report_id,
                pass_to_account=pass_to_account,
                retry=True,
                retry_count=3,
                duplicate_protection=duplicate_protection
            )
            if df is not None:
                self.report_data[report_name] = df
            self.reports[report_name].set()
        except Exception as e:
            logging.error(f"Error fetching report {report_name}: {e}")
            self.reports.pop(report_name, None)
            self.report_data.pop(report_name, None)
            raise
    
    async def fetch_files_from_blob(self, folder_path: str) -> tuple:
        """
        Fetches a list of file names from a specific folder within an Azure Blob Storage container.

        Args:
            folder_path (str): The path to the folder inside the container (e.g., 'A107DACC-9BCE-40A0-8E98-A88F529F6B7D/tagsyncing_backups/').
                            Ensure the path ends with a '/'.

        Returns:
            tuple: A tuple containing a boolean flag indicating if the folder exists and a list of file names found in the specified folder.
                Returns (False, []) if the folder does not exist or if there's an error.
        """
        try:
            # Create a BlobServiceClient object
            blob_service_client = BlobServiceClient.from_connection_string(self.blob_connection_string)
            # Get a ContainerClient object
            container_client = blob_service_client.get_container_client(self.blob_container_name)
            folder_path = self.tenantid + '/' + folder_path

            file_list = []
            folder_exists = False

            # List the blobs in the container with the specified prefix (folder path)
            async for blob in container_client.list_blobs(name_starts_with=folder_path):
                folder_exists = True
                file_list.append(blob.name.split('/')[-1])  # Extract just the file name

            return folder_exists, file_list

        except Exception as e:
            logging.error(f"An error occurred: {e}")
            return False, []


    async def delete_blob_file(self, blob_path: str) -> bool:
        """
        Deletes a specific blob (file) from a folder within an Azure Blob Storage container.

        Args:
            folder_path (str): The path to the folder inside the container (e.g., 'tagsyncing_backups/').
                            Do not include tenantid here — it will be prepended automatically.
            file_name (str): The name of the file to be deleted (e.g., 'backup1.json').

        Returns:
            bool: True if deletion was successful, False otherwise.
        """
        try:
            # Create a BlobServiceClient object
            blob_service_client = BlobServiceClient.from_connection_string(self.blob_connection_string)
            # Get a ContainerClient object
            container_client = blob_service_client.get_container_client(self.blob_container_name)
            
            # Construct the full blob path (tenantid/folder/file)
            blob_path = f"{self.tenantid}/{blob_path}"

            # Get a BlobClient for the specific file
            blob_client = container_client.get_blob_client(blob_path)

            # Delete the blob
            await blob_client.delete_blob()
            logging.info(f"Successfully deleted blob: {blob_path}")
            return True

        except Exception as e:
            logging.error(f"Failed to delete blob: {blob_path}. Error: {e}")
            return False

    async def cleanup(self):
        """Cleanup resources"""
        if self.redis:
            await self.redis.close()
        for task in self._background_tasks:
            if not task.done():
                task.cancel()