"""SharePoint Handler Module.

This module provides comprehensive SharePoint integration functionality for the JL Automation platform.
It includes document management, email operations, blob storage integration, and various SharePoint
operations such as file upload/download, folder management, and content manipulation.

Key Features:
    - SharePoint document library operations
    - Azure Blob Storage integration
    - Email sending via SendGrid
    - File upload/download with multiple formats
    - Folder creation and management
    - Access token management with automatic renewal
    - Both synchronous and asynchronous operations
    - Support for various content types (text, HTML, binary)

The module handles authentication, token management, and provides both legacy synchronous
methods and modern asynchronous methods for better performance in concurrent environments.

Example:
    Basic usage:
        sp_manager = SharePointManager(config)
        
        # Upload a file
        await sp_manager.upload_file_to_sharepoint_async(file_path, "Documents/report.pdf")
        
        # Download blob content
        content = sp_manager.download_blob_content_synchronous("container", "file.txt")
        
        # Send email
        await sp_manager.send_email_async("recipient@example.com", "Subject", "Message")
"""

from datetime import datetime, timedelta, timezone
import logging
import pytz
import os
import pandas as pd
import base64
import mimetypes
import aiohttp
import asyncio
import requests
import time
from azure.storage.blob import BlobServiceClient

from .appconfig_handler import Configurations
from enum import Enum
from requests.exceptions import RequestException
import json
from .common_utility import deprecated



class GraphMethod(Enum):
    """Enumeration for HTTP request methods used in Graph API calls.

    Values map directly to the HTTP verb strings expected by ``aiohttp``,
    following the same pattern as ``RequestType`` in ``mainsubsysapi_handler``.

    Members:
        GET: Retrieve data.
        POST: Create new resources.
        PUT: Replace/upload resources.
        PATCH: Partially update resources.
        DELETE: Remove resources.
    """
    GET = "GET"
    POST = "POST"
    PUT = "PUT"
    PATCH = "PATCH"
    DELETE = "DELETE"


async def graph_request(
        url: str,
        headers: dict,
        method: GraphMethod = GraphMethod.GET,
        max_retries: int = 5,
        session: aiohttp.ClientSession = None,
        **kwargs,
    ):
    """Reusable aiohttp wrapper for Microsoft Graph API calls.

    Handles 429 Too Many Requests by:
      - Honouring the ``Retry-After`` response header when present.
      - Falling back to exponential backoff (2 ** attempt seconds) when absent.

    Args:
        url (str): The full Graph API endpoint URL.
        headers (dict): HTTP headers to include (e.g. Authorization bearer token).
        method (GraphMethod): HTTP verb enum member – ``GraphMethod.GET``,
            ``GraphMethod.POST``, ``GraphMethod.PUT``, ``GraphMethod.PATCH``,
            ``GraphMethod.DELETE``.  Defaults to ``GraphMethod.GET``.
        max_retries (int): Maximum number of retry attempts on 429 responses.
            Defaults to 5.
        session (aiohttp.ClientSession | None): An existing session to reuse.
            When ``None`` a new session is created and closed automatically.
        **kwargs: Extra keyword arguments forwarded directly to the aiohttp
            request call (e.g. ``json=``, ``data=``).

    Returns:
        tuple[int, Any]: A ``(status_code, body)`` tuple where ``body`` is the
        parsed JSON response or raw text if JSON decoding fails.

    Raises:
        RequestException: On non-200/429 status codes or when ``max_retries``
            is exhausted.
    """
    _method = method.value

    async def _execute(sess: aiohttp.ClientSession):
        retries = 0
        while retries < max_retries:
            async with sess.request(_method, url, headers=headers, **kwargs) as response:
                if response.status in (200, 201, 202, 204):
                    # Read and return parsed body so the caller doesn't need to
                    # re-read a closed response.
                    try:
                        body = await response.json()
                    except Exception:
                        body = await response.text()
                    return response.status, body

                if response.status == 429:
                    retry_after_raw = response.headers.get("Retry-After", "")
                    if retry_after_raw.isdigit():
                        wait = int(retry_after_raw)
                        logging.warning(
                            f"Graph API rate-limited (429). Honouring Retry-After: "
                            f"{wait}s (attempt {retries + 1}/{max_retries}) – {url}"
                        )
                    else:
                        wait = 2 ** retries
                        logging.warning(
                            f"Graph API rate-limited (429). No Retry-After header; "
                            f"exponential backoff: {wait}s (attempt {retries + 1}/{max_retries}) – {url}"
                        )
                    await asyncio.sleep(wait)
                    retries += 1
                    continue

                # Any other non-success status – raise immediately.
                text = await response.text()
                logging.error(
                    f"Graph API {_method} {url} failed with status "
                    f"{response.status}: {text}"
                )
                raise RequestException(text)

        raise RequestException(
            f"Max retries ({max_retries}) exceeded due to throttling for {_method} {url}"
        )

    if session is not None:
        return await _execute(session)

    async with aiohttp.ClientSession() as new_session:
        return await _execute(new_session)



class ContentType(Enum):
    """Enumeration for content types used in SharePoint operations.
    
    This enum defines the supported content types for various SharePoint
    document and content operations.
    """
    Text = "text"
    HTML = "html"


class SharePointManager:
    """Comprehensive SharePoint and Azure integration manager.
    
    This class provides a unified interface for SharePoint operations, Azure Blob Storage
    interactions, and email functionality. It handles authentication, token management,
    and provides both synchronous and asynchronous methods for various operations.
    
    The manager automatically handles access token renewal and provides error handling
    for common SharePoint and Azure operations.
    
    Attributes:
        API (Dict[str, Any]): SharePoint API configuration.
        SAS (str): Azure Storage connection string.
        FORMAT (str): DateTime format string for SharePoint operations.
        AUTOMATION_DRIVE (str): SharePoint drive ID for automation documents.
        _access_token (Optional[str]): Current access token for async operations.
        _access_token_synchronous (Optional[str]): Current access token for sync operations.
        _token_expiration (datetime): Token expiration timestamp.
        SENDGRID (Dict[str, Any]): SendGrid email configuration.
        
    Example:
        manager = SharePointManager(config)
        content = manager.download_blob_content_synchronous("container", "file.txt")
        await manager.send_email_async("user@example.com", "Subject", "Content")
    """
    
    def __init__(self, config: Configurations):
        """Initialize the SharePointManager with configuration settings.
        
        Args:
            config (Configurations): Configuration object containing SharePoint,
                                   Azure Storage, and SendGrid settings.
                                   
        Raises:
            json.JSONDecodeError: If configuration JSON parsing fails.
            ValueError: If required configuration values are missing.
        """
        self.API = json.loads(config.Sharepoint)
        self.SAS =  config.StorageConnectionString
        self.FORMAT = "%Y-%m-%dT%H:%M:%SZ"
        self.AUTOMATION_DRIVE = "b!yFgeUj7tBEuaC25igUcgHWWUUQPEV8FNtm4y7Pv-QvBIXJWydNmpRbib-m4pSf-K"
        
        # Token management
        self._access_token = None
        self._access_token_synchronous = None
        self._token_expiration = datetime.now(timezone.utc)
        
        # Email configuration
        self.SENDGRID = json.loads(config.send_grid_email)

    def download_blob_content_synchronous(self, folder_name, file_path):
        """Download blob content from Azure Storage synchronously.
        
        This method downloads content from an Azure Blob Storage container
        using the configured SAS connection string.
        
        Args:
            folder_name (str): Name of the blob container.
            file_path (str): Path to the file within the container.
            
        Returns:
            Any: Downloaded blob content object from Azure Storage SDK.
            
        Raises:
            ValueError: If SAS URL is missing or empty.
            Exception: If blob download operation fails.
            
        Example:
            content = manager.download_blob_content_synchronous("documents", "reports/monthly.pdf")
            with open("local_file.pdf", "wb") as f:
                f.write(content.readall())
        """
        try: 
            if not self.SAS: 
                raise ValueError("SAS URL is missing or empty.")
            
            with BlobServiceClient(account_url=self.SAS) as blob_service_client:
                container_client = blob_service_client.get_container_client(folder_name)
                file_path = file_path.replace("\\", "/")
                blob_client = container_client.get_blob_client(file_path)
                downloaded_blob = blob_client.download_blob()
                return downloaded_blob
        except Exception as e:
            logging.error(f"An error occurred on downloading blob content synchronously: {str(e)}")
            raise

    def get_signature_html_synchronous(self):
        try:
            file_stream = self.download_blob_content_synchronous("html_signature", "signature_block.html")
            html_signature = file_stream.readall()
            html_signature = html_signature.decode('utf-8')
            return html_signature.replace('\r\n', '').strip()
        except Exception as e:
            logging.error(f"An error occurred on getting signature html synchronously: {str(e)}")
            raise

    async def get_sharepoint_access_token(self) -> str:
        """Returns a cached token if it's still valid, otherwise fetches a new one."""
        
        if self._access_token and datetime.now(timezone.utc) < self._token_expiration:
            return self._access_token
        
        token_endpoint = f'https://login.microsoftonline.com/{self.API["tenant_id"]}/oauth2/v2.0/token'
        token_data = {
            "grant_type": "client_credentials",
            "client_id": self.API["client_id"],
            "client_secret": self.API["client_secret"],
            "scope": "https://graph.microsoft.com/.default",
        }
        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(token_endpoint, data=token_data) as response:
                    if response.status == 200:
                        token_response = await response.json()
                        self._access_token = token_response.get("access_token")
                        
                        # Use expires_in and ext_expires_in to set expiration
                        # Typically 3599 seconds (~1 hour), so use a buffer (e.g., 50 minutes)
                        # so we fetch a fresh token before it fully expires.
                        buffer_seconds = 10 * 60  # 10-minute buffer
                        expires_in = token_response.get('expires_in', 3600)
                        
                        # Set new expiration time (current time + expires_in - buffer)
                        self._token_expiration = datetime.now(timezone.utc) + timedelta(seconds=(expires_in - buffer_seconds))

                        return self._access_token
                    else:
                        text = await response.text()
                        logging.error(f"Failed to get access token: {text}")
                        raise RequestException(text)
        except Exception as e:
            logging.error(f"An error occurred while fetching access token: {e}")
            raise

    async def get_sharepoint_data(self, folder_url=None, short_path=None, max_retries=5):
        try:
            if short_path is not None:
                folder_url = self.API["onedrive"] + f"{short_path}:/children"

            token = await self.get_sharepoint_access_token()
            headers = {"Authorization": f"Bearer {token}"}

            _, data = await graph_request(folder_url, headers, max_retries=max_retries)
            file_data = data.get("value")
            return pd.DataFrame(file_data)
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            raise e

    async def send_email(self, subject: str, body, recipient, attachments=None, content_type: ContentType = ContentType.Text, custom_signature: str = None):
        try:
            html_signature = self.get_signature_html_synchronous()
            token = await self.get_sharepoint_access_token()
            url = "https://graph.microsoft.com/v1.0/users/Automation@joblogic.com/sendMail"
            headers = {
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
            }
            to_recipients = [{"emailAddress": {"address": email}} for email in recipient]
            concatenated_body = "\n".join(body)

            # Handle signature appending
            if not custom_signature:
                if content_type == ContentType.HTML:
                    concatenated_body += html_signature 
                else:
                    concatenated_body = concatenated_body.replace('\n', '<br>')
                    concatenated_body = f"<html><body><p>{concatenated_body}</p>{html_signature}</body></html>"
                    content_type = ContentType.HTML
            else:
                concatenated_body += f"\n\n{custom_signature}"

            payload = {
                "message": {
                    "subject": subject,
                    "body": {"contentType": content_type.value, "content": concatenated_body},
                    "toRecipients": to_recipients,
                },
                "saveToSentItems": True,
            }
            if attachments:
                payload["message"]["attachments"] = []
                for attachment_path in attachments:
                    attachment_name = os.path.basename(attachment_path)
                    if os.path.exists(attachment_path):
                        with open(attachment_path, "rb") as attachment:
                            content_bytes = base64.b64encode(attachment.read()).decode()
                            payload["message"]["attachments"].append(
                                {
                                    "@odata.type": "#microsoft.graph.fileAttachment",
                                    "name": attachment_name,
                                    "contentType": mimetypes.guess_type(attachment_name)[0] or "application/octet-stream",
                                    "contentBytes": content_bytes,
                                }
                            )
            async with aiohttp.ClientSession() as session:
                async with session.post(url, headers=headers, json=payload) as response:
                    if response.status != 202:
                        text = await response.text()
                        logging.error(f"Failed to send email: {text}")
                        raise RequestException(text)
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            raise e

    @deprecated("Use upload_file_to_drive instead as this method is deprecated.")
    async def upload_file(self, excel_file_path, folder_id, excel_name):
        try:
            access_token = await self.get_sharepoint_access_token()
            with open(excel_file_path, "rb") as upload_file:
                file_content = upload_file.read()
            upload_url = f"https://graph.microsoft.com/v1.0/users/Automation@joblogic.com/drives/{self.AUTOMATION_DRIVE}/items/{folder_id}:/{excel_name}:/content"
            headers = {
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/octet-stream",
            }
            async with aiohttp.ClientSession() as session:
                async with session.put(upload_url, headers=headers, data=file_content) as response:
                    return response
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            raise e
        
    async def upload_file_to_drive(self, folder_path: str, file_name: str, file_path: str=None, file_content: bytes=None) -> aiohttp.ClientResponse:
        try:
            if file_path:
                with open(file_path, "rb") as f:
                    file_content = f.read()
            elif file_content is None:
                raise ValueError("Either file_path or file_content must be provided.")

            upload_url = self.API["onedrive"] + f"{folder_path}/{file_name}:/content"
            headers = {"Authorization": f"Bearer {await self.get_sharepoint_access_token()}", "Content-Type": "application/octet-stream"}

            async with aiohttp.ClientSession() as session:
                async with session.put(upload_url, headers=headers, data=file_content) as response:
                    return response

        except Exception as e:
            logging.error(f"Failed to upload file '{file_name or file_path}': {e}")
            raise e
    
    async def upload_zip_file(self, file_path, folder_id, file_name):
        try:
            access_token = await self.get_sharepoint_access_token()
            with open(file_path, 'rb') as upload_file:
                file_content = upload_file.read()
            upload_url = f"https://graph.microsoft.com/v1.0/users/Automation@joblogic.com/drives/{self.AUTOMATION_DRIVE}/items/{folder_id}:/{file_name}:/content"
            headers = {
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/zip"
            }
            async with aiohttp.ClientSession() as session:
                async with session.put(upload_url, headers=headers, data=file_content) as response:
                    if response.status == 201:
                        logging.info(f"File '{file_name}' uploaded successfully.")
                    elif response.status == 200:
                        logging.warning(f"File '{file_name}' already exists.")
                    else:
                        text = await response.text()
                        logging.error(f"Failed to upload file '{file_name}'. Status Code: {response.status}, Error: {text}")
                    response_json = await response.json()
                    return response_json

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

    def subtract_hour_if_dst_uk_time(self, timestamp):
        try:
            dt_object = datetime.strptime(timestamp, self.FORMAT)
            uk_timezone = pytz.timezone('Europe/London')
            dt_object_uk = uk_timezone.localize(dt_object)
            is_dst = dt_object_uk.dst() != timedelta(0)
            if is_dst:
                dt_object_uk -= timedelta(hours=1)
            formatted_date = dt_object_uk.strftime(self.FORMAT)
            return formatted_date
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            raise e

    async def get_all_emails_by_user_and_datetime(self, token, user_mail, from_email, send_date_time, end_date_time=None):
        try:
            formatted_date = send_date_time.strftime(self.FORMAT)
            url = f"https://graph.microsoft.com/v1.0/users/{user_mail}/mailFolders/inbox/messages?$filter=from/emailAddress/address eq '{from_email}' and sentDateTime ge {formatted_date}"

            if end_date_time:
                formatted_end_date = end_date_time.strftime(self.FORMAT)
                url += f" and sentDateTime lt {formatted_end_date}"

            headers = {"Authorization": f"Bearer {token}"}

            emails = []
            async with aiohttp.ClientSession() as session:
                while url:
                    _, data = await graph_request(url, headers, session=session)
                    emails.extend(data.get('value', []))
                    url = data.get('@odata.nextLink')
            return emails
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            raise e

    async def get_all_emails_by_domains_and_datetime(self, token, user_mail, from_domains, send_date_time, end_date_time=None):
        try:
            formatted_date = send_date_time.strftime(self.FORMAT)
            domain_filter = " or ".join([f"contains(from/emailAddress/address, '{domain}')" for domain in from_domains])
            url = f"https://graph.microsoft.com/v1.0/users/{user_mail}/mailFolders/inbox/messages?$filter=({domain_filter}) and sentDateTime ge {formatted_date}"

            if end_date_time:
                formatted_end_date = end_date_time.strftime(self.FORMAT)
                url += f" and sentDateTime lt {formatted_end_date}"

            headers = {"Authorization": f"Bearer {token}"}

            emails = []
            async with aiohttp.ClientSession() as session:
                while url:
                    _, data = await graph_request(url, headers, session=session)
                    emails.extend(data.get('value', []))
                    url = data.get('@odata.nextLink')
            return emails
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            raise e

    @staticmethod
    async def get_email_attachments_by_user_and_message_id(token, user_mail, message_id, retries=3, delay=10):
        try:
            url = f"https://graph.microsoft.com/v1.0/users/{user_mail}/mailFolders/inbox/messages/{message_id}/attachments"
            headers = {"Authorization": f"Bearer {token}"}
            async with aiohttp.ClientSession() as session:
                for attempt in range(retries):
                    async with session.get(url, headers=headers) as response:
                        if response.status == 200:
                            attachments = await response.json()
                            if 'value' in attachments and attachments['value']:
                                return attachments
                            else:
                                logging.info(f"Attempt {attempt + 1}/{retries}: No attachments found for message ID {message_id}. Retrying in {delay} seconds...")
                                await asyncio.sleep(delay)
                        elif response.status == 429:
                            logging.info(f"Attempt {attempt + 1}/{retries}: Rate limit exceeded. Retrying in {delay} seconds...")
                            await asyncio.sleep(delay)

                        elif 500 <= response.status < 600:
                            # Server errors (5xx), likely temporary issues on the server side
                            logging.warning(f"Attempt {attempt + 1}/{retries}: Server error {response.status}. Retrying in {delay} seconds...")
                            await asyncio.sleep(delay)
                        elif response.status == 404:
                            logging.warning(f"Attempt {attempt + 1}/{retries}: Resource not found (404). No further retries needed.")
                            raise FileNotFoundError(f"Attachment or message not found for message ID {message_id}.")
                        else:
                            logging.warning(f"Attempt {attempt + 1}: Failed to retrieve attachments. Status code {response.status}. Error: {response.text}")
                            response.raise_for_status()
                
                logging.error(f"Failed to retrieve attachments after {retries} attempts for message ID {message_id}.")
                raise RequestException(f"Failed to retrieve attachments after {retries} attempts for message ID {message_id}.")
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            raise

    @staticmethod
    async def download_attachment(access_token, user_mail, message_id, attachment_id):
        """Download raw attachment bytes, with throttling protection via ``graph_request``."""
        try:
            headers = {
                'Authorization': f'Bearer {access_token}',
                'Content-Type': 'application/json'
            }
            url = f"https://graph.microsoft.com/v1.0/users/{user_mail}/mailFolders/inbox/messages/{message_id}/attachments/{attachment_id}/$value"

            # graph_request returns (status, body); for a $value endpoint the body
            # is raw bytes / text rather than JSON – we re-fetch via a plain GET
            # inside a session so we can call response.read().
            async with aiohttp.ClientSession() as session:
                retries = 0
                max_retries = 5
                while retries < max_retries:
                    async with session.get(url, headers=headers) as response:
                        if response.status == 200:
                            return await response.read()
                        if response.status == 429:
                            retry_after_raw = response.headers.get("Retry-After", "")
                            wait = int(retry_after_raw) if retry_after_raw.isdigit() else 2 ** retries
                            logging.warning(
                                f"download_attachment rate-limited (429). "
                                f"Waiting {wait}s (attempt {retries + 1}/{max_retries})."
                            )
                            await asyncio.sleep(wait)
                            retries += 1
                            continue
                        text = await response.text()
                        logging.error(f"{response.status} - {text}")
                        return None
                raise RequestException(
                    f"Max retries ({max_retries}) exceeded downloading attachment {attachment_id}."
                )
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            raise e

    async def send_mail_with_content(self, subject: str, body, recipient, content, attachment_name, email_content_type: ContentType = ContentType.Text, custom_signature: str = None):
        try:
            html_signature = self.get_signature_html_synchronous()
            if isinstance(content, list) and isinstance(attachment_name, list):
                if len(content) != len(attachment_name):
                    raise ValueError("The number of items in 'content' and 'attachment_name' must match.")
            elif not isinstance(content, list):
                content = [content]
                attachment_name = [attachment_name]

            if not content or not all(content):
                raise ValueError("Content for the attachments cannot be None or empty.")
            
            token = await self.get_sharepoint_access_token()
            url = "https://graph.microsoft.com/v1.0/users/Automation@joblogic.com/sendMail"
            headers = {
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
            }
            to_recipients = [{"emailAddress": {"address": email}} for email in recipient]
            concatenated_body = "\n".join(body)

            if not custom_signature:
                if email_content_type == ContentType.HTML:
                    concatenated_body += html_signature 
                else:
                    concatenated_body = concatenated_body.replace('\n', '<br>')
                    concatenated_body = f"<html><body><p>{concatenated_body}</p>{html_signature}</body></html>"
                    email_content_type = ContentType.HTML
            else:
                concatenated_body += f"\n\n{custom_signature}"

            attachments = []
            for i, content_item in enumerate(content):
                content_type, _ = mimetypes.guess_type(attachment_name[i])
                if not content_type:
                    content_type = 'application/octet-stream'
                attachments.append({
                    "@odata.type": "#microsoft.graph.fileAttachment",
                    "name": attachment_name[i],
                    "contentType": content_type,
                    "contentBytes": content_item,
                })
            payload = {
                "message": {
                    "subject": subject,
                    "body": {"contentType": email_content_type.value, "content": concatenated_body},
                    "toRecipients": to_recipients,
                    "attachments": attachments
                },
                "saveToSentItems": True,
            }
            async with aiohttp.ClientSession() as session:
                async with session.post(url, headers=headers, json=payload) as response:
                    if response.status != 202:
                        text = await response.text()
                        logging.error(f"Failed to send email with content: {text}")
                        raise RequestException(text)
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            raise e

    async def search_items_in_drive(self, folder_name):
        try:
            access_token = await self.get_sharepoint_access_token()
            api_url = f"https://graph.microsoft.com/v1.0/users/Automation@joblogic.com/drive/root/search(q='{folder_name}')"
            headers = {"Authorization": f"Bearer {access_token}"}

            _, data = await graph_request(api_url, headers)
            return data
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            raise e

    def send_email_synchronous(
        self,
        subject: str,
        body,
        recipient,
        attachments=None,
        content_type: ContentType = ContentType.Text,
        delay=0,
        max_retries=3,
        custom_signature: str = None,
    ):
        attempt = 0
        while attempt < max_retries:
            try:
                # Sleep before attempting to send the email (if necessary)
                time.sleep(delay)

                html_signature = self.get_signature_html_synchronous()
                token = self.get_sharepoint_access_token_synchronus()  # Assuming this method is synchronous
                url = "https://graph.microsoft.com/v1.0/users/Automation@joblogic.com/sendMail"
                headers = {
                    "Authorization": f"Bearer {token}",
                    "Content-Type": "application/json",
                }

                to_recipients = [{"emailAddress": {"address": email}} for email in recipient]
                concatenated_body = "\n".join(body)

                # Append signature
                if not custom_signature:
                    if content_type == ContentType.HTML:
                        concatenated_body += html_signature 
                    else:
                        concatenated_body = concatenated_body.replace('\n', '<br>')
                        concatenated_body = f"<html><body><p>{concatenated_body}</p>{html_signature}</body></html>"
                        content_type = ContentType.HTML
                else:
                    concatenated_body += f"\n\n{custom_signature}"

                payload = {
                    "message": {
                        "subject": subject,
                        "body": {"contentType": content_type.value, "content": concatenated_body},
                        "toRecipients": to_recipients,
                    },
                    "saveToSentItems": True,
                }

                if attachments:
                    payload["message"]["attachments"] = []
                    for attachment_path in attachments:
                        attachment_name = os.path.basename(attachment_path)
                        if os.path.exists(attachment_path):
                            with open(attachment_path, "rb") as attachment:
                                content_bytes = base64.b64encode(attachment.read()).decode()
                                payload["message"]["attachments"].append(
                                    {
                                        "@odata.type": "#microsoft.graph.fileAttachment",
                                        "name": attachment_name,
                                        "contentType": mimetypes.guess_type(attachment_name)[0] or "application/octet-stream",
                                        "contentBytes": content_bytes,
                                    }
                                )

                response = requests.post(url, headers=headers, json=payload)
                if response.status_code != 202:
                    text = response.text
                    logging.error(f"Failed to send email: {text}")
                    raise RequestException(text)

                return # Email Sent Successfully

            except Exception as e:
                attempt += 1
                logging.error(f"Attempt {attempt} failed: {e}")
                if attempt < max_retries:
                    backoff_time = 2 ** attempt  # Exponential backoff: 1, 2, 4, ...
                    logging.info(f"Retrying in {backoff_time} seconds...")
                    time.sleep(backoff_time)
                else:
                    logging.error("Max retries reached, giving up.")
                    raise e

    def get_sharepoint_access_token_synchronus(self) -> str:
        """Returns a cached token if it's still valid, otherwise fetches a new one."""

        if self._access_token_synchronous and datetime.now(timezone.utc) < self._token_expiration:
            return self._access_token_synchronous

        token_endpoint = f'https://login.microsoftonline.com/{self.API["tenant_id"]}/oauth2/v2.0/token'
        token_data = {
            "grant_type": "client_credentials",
            "client_id": self.API["client_id"],
            "client_secret": self.API["client_secret"],
            "scope": "https://graph.microsoft.com/.default",
        }

        try:
            response = requests.post(token_endpoint, data=token_data)

            if response.status_code == 200:
                token_response = response.json()
                self._access_token_synchronous = token_response.get("access_token")

                buffer_seconds = 10 * 60  # 10-minute buffer
                expires_in = token_response.get('expires_in', 3600)

                # Set new expiration time (current time + expires_in - buffer)
                self._token_expiration = datetime.now(timezone.utc) + timedelta(seconds=(expires_in - buffer_seconds))

                return self._access_token_synchronous
            else:
                text = response.text
                logging.error(f"Failed to get access token: {text}")
                raise RequestException(text)

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

    def send_mail_with_content_synchronous(
        self,
        subject: str,
        body,
        recipient,
        content,
        attachment_name,
        email_content_type: ContentType = ContentType.Text,
        delay=0,
        max_retries=3,
        custom_signature: str = None,
        ccRecipients = None,
    ):
        attempt = 0
        while attempt < max_retries:
            try:
                # Sleep before attempting to send the email (if necessary)
                time.sleep(delay)

                html_signature = self.get_signature_html_synchronous()
                if isinstance(content, list) and isinstance(attachment_name, list):
                    if len(content) != len(attachment_name):
                        raise ValueError("The number of items in 'content' and 'attachment_name' must match.")
                elif not isinstance(content, list):
                    content = [content]
                    attachment_name = [attachment_name]
                if not content or not all(content):
                    raise ValueError("Content for the attachments cannot be None or empty.")

                token = self.get_sharepoint_access_token_synchronus()  # Assuming this method is synchronous
                url = "https://graph.microsoft.com/v1.0/users/Automation@joblogic.com/sendMail"
                headers = {
                    "Authorization": f"Bearer {token}",
                    "Content-Type": "application/json",
                }

                to_recipients = [{"emailAddress": {"address": email}} for email in recipient]
                concatenated_body = "\n".join(body)

                # Now append the signature
                if not custom_signature:
                    if email_content_type == ContentType.HTML:
                        concatenated_body += html_signature 
                    else:
                        concatenated_body = concatenated_body.replace('\n', '<br>')
                        concatenated_body = f"<html><body><p>{concatenated_body}</p>{html_signature}</body></html>"
                        email_content_type = ContentType.HTML
                else:
                    concatenated_body += f"\n\n{custom_signature}"

                attachments = []
                for i, content_item in enumerate(content):
                    content_type, _ = mimetypes.guess_type(attachment_name[i])
                    if not content_type:
                        content_type = 'application/octet-stream'
                    attachments.append({
                        "@odata.type": "#microsoft.graph.fileAttachment",
                        "name": attachment_name[i],
                        "contentType": content_type,
                        "contentBytes": content_item,
                    })

                payload = {
                    "message": {
                        "subject": subject,
                        "body": {"contentType": email_content_type.value, "content": concatenated_body},
                        "toRecipients": to_recipients,
                        "ccRecipients": [{"emailAddress": {"address": email}} for email in ccRecipients] if ccRecipients else [],
                        "attachments": attachments
                    },
                    "saveToSentItems": True,
                }

                response = requests.post(url, headers=headers, json=payload)
                if response.status_code != 202:
                    text = response.text
                    logging.error(f"Failed to send email with content: {text}")
                    raise RequestException(text)

                return  # Successfully sent email

            except Exception as e:
                attempt += 1
                logging.error(f"Attempt {attempt} failed: {e}")
                if attempt < max_retries:
                    backoff_time = 2 ** attempt  # Exponential backoff: 1, 2, 4, ...
                    logging.info(f"Retrying in {backoff_time} seconds...")
                    time.sleep(backoff_time)
                else:
                    logging.error("Max retries reached, giving up.")
                    raise e

    async def send_mail_with_content_using_sendgrid(
            self,
            subject: str,
            body,
            recipient,
            content,
            attachment_name,
            email_content_type: ContentType = ContentType.Text,
            custom_signature: str = None,
    ):
        try:
            html_signature = self.get_signature_html_synchronous()
            if isinstance(content, list) and isinstance(attachment_name, list):
                if len(content) != len(attachment_name):
                    raise ValueError("The number of items in 'content' and 'attachment_name' must match.")
            elif not isinstance(content, list):
                content = [content]
                attachment_name = [attachment_name]

            if not content or not all(content):
                raise ValueError("Content for the attachments cannot be None or empty.")

            headers = {
                "Authorization": f"Bearer {self.SENDGRID['sendgrid_secret']}",
                "Content-Type": "application/json",
            }

            # Prepare recipients
            to_recipients = [{"email": email} for email in recipient]

            # Prepare email body
            if isinstance(body, list):
                concatenated_body = "\n".join(body)
            else:
                concatenated_body = body

            # Append signature
            if not custom_signature:
                if email_content_type == ContentType.HTML:
                    concatenated_body += html_signature 
                else:
                    concatenated_body = concatenated_body.replace('\n', '<br>')
                    concatenated_body = f"<html><body><p>{concatenated_body}</p>{html_signature}</body></html>"
            else:
                concatenated_body += f"\n{custom_signature}".strip()

            # Prepare attachments
            attachments = []
            for i, content_item in enumerate(content):
                # Ensure content is base64 encoded
                if isinstance(content_item, bytes):
                    content_base64 = base64.b64encode(content_item).decode()
                elif isinstance(content_item, str):
                    try:
                        base64.b64decode(content_item)
                        content_base64 = content_item  # Already base64
                    except Exception:
                        content_base64 = base64.b64encode(content_item.encode()).decode()
                else:
                    raise ValueError("Attachment content must be str or bytes.")

                # Guess MIME type
                content_type, _ = mimetypes.guess_type(attachment_name[i])
                if not content_type:
                    content_type = 'application/octet-stream'

                attachments.append({
                    "content": content_base64,
                    "type": content_type,
                    "filename": attachment_name[i],
                    "disposition": "attachment",
                })

            # Prepare full payload
            payload = {
                "personalizations": [
                    {"to": to_recipients}
                ],
                "from": {"email": "automation@joblogic.com"},
                "subject": subject,
                "content": [
                    {
                        "type": "text/html",
                        "value": concatenated_body
                    }
                ],
                "attachments": attachments
            }

            # Send email
            async with aiohttp.ClientSession() as session:
                async with session.post(self.SENDGRID['sendgrid_url'], headers=headers, json=payload) as response:
                    if response.status != 202:
                        text = await response.text()
                        logging.error(f"Failed to send email with SendGrid: {text}")
                        raise RequestException(text)
                    else:
                        logging.info("Email sent successfully with SendGrid!")

        except Exception as e:
            logging.error(f"An error occurred while sending email with SendGrid: {e}")
            raise e

    async def create_sharing_link(self, item_id):
        """Create a sharing link for the item that can be viewed in a browser."""
        try:
            access_token = await self.get_sharepoint_access_token()

            url = f"https://graph.microsoft.com/v1.0/drives/{self.AUTOMATION_DRIVE}/items/{item_id}/createLink"

            # This will create a link that's accessible within your organization
            payload = {
                "type": "view",
                "scope": "anonymous"
            }

            headers = {
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json"
            }

            async with aiohttp.ClientSession() as session:
                async with session.post(url, headers=headers, json=payload) as response:
                    if response.status in [200, 201]:
                        response_data = await response.json()
                        # The sharing URL is in the 'link' object's 'webUrl' property
                        if 'link' in response_data and 'webUrl' in response_data['link']:
                            logging.info(f"Sharing link created successfully")
                            return response_data['link']['webUrl']
                        else:
                            logging.warning("Sharing link structure not found in response")
                            return None
                    else:
                        error_text = await response.text()
                        logging.error(f"Failed to create sharing link. Status: {response.status}, Error: {error_text}")
                        return None

        except Exception as e:
            logging.error(f"Error creating sharing link: {e}")
            return None

    def get_subscription_expiration_time(self, minutes: int = 4230) -> str:
        new_datetime = datetime.utcnow() + timedelta(minutes=minutes)
        return new_datetime.replace(microsecond=0).strftime("%Y-%m-%dT%H:%M:%SZ")

    async def create_email_subscription(self,payload: dict) -> dict:
        # Validate the required keys in the payload
        required_keys = ["changeType", "notificationUrl", "lifecycleNotificationUrl", "resource", "clientState","expirationDateTime"]
        missing_keys = [key for key in required_keys if key not in payload]

        if missing_keys:
            raise ValueError(f"Missing required keys in the payload: {', '.join(missing_keys)}")

        token = await self.get_sharepoint_access_token()
        headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

        # Send request to create the subscription
        async with aiohttp.ClientSession() as session:
            async with session.post("https://graph.microsoft.com/v1.0/subscriptions", headers=headers, json=payload) as response:
                if response.status == 201:
                    logging.info("Email subscription created successfully.")
                    return await response.json()

                error_detail = await response.text()
                logging.error(f"Failed to create subscription: {response.status} - {error_detail}")
                raise Exception("Failed to create email subscription.")

    async def delete_subscription(self, subscription_id: str) -> bool:
        token = await self.get_sharepoint_access_token()
        url = f"https://graph.microsoft.com/v1.0/subscriptions/{subscription_id}"
        headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
        async with aiohttp.ClientSession() as session:
            async with session.delete(url, headers=headers) as response:
                if response.status in [204, 404]:
                    logging.info(f"Deleted subscription: {subscription_id}")
                    return True
                error_detail = await response.text()
                logging.error(f"Failed to delete subscription {subscription_id}: {response.status} - {error_detail}")
                raise Exception("Failed to delete subscription.")

    async def get_all_subscriptions(self) -> list:
        token = await self.get_sharepoint_access_token()
        headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
        _, data = await graph_request(
            "https://graph.microsoft.com/v1.0/subscriptions", headers
        )
        return data.get("value", [])
            
    
    async def update_subscription_expiration(self, subscription_id: str, expiration_datetime: str) -> dict:
        try:
            # Create the payload with the new expirationDateTime
            payload = {
                "expirationDateTime": expiration_datetime
            }

            token = await self.get_sharepoint_access_token()
            url = f"https://graph.microsoft.com/v1.0/subscriptions/{subscription_id}"
            headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

            # Send the PATCH request to update the subscription
            async with aiohttp.ClientSession() as session:
                async with session.patch(url, headers=headers, json=payload) as response:
                    if response.status == 200:
                        logging.info(f"Subscription {subscription_id} updated successfully.")
                        return await response.json()
                    
                    response.raise_for_status()
        
        except Exception as e:
            logging.error(f"An error occurred while updating subscription {subscription_id}: {str(e)}")
            raise