import requests
import secrets
import string
import traceback
import re, os, io
from io import BytesIO

from django.core.files.base import ContentFile
from django.core.files.storage import Storage
from django.core.files import File
from django.utils.deconstruct import deconstructible
from django.utils.deconstruct import deconstructible

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload
from googleapiclient.errors import HttpError
from cloud_storages.utils import *

_DEFAULT_TIMEOUT = 100
_DEFAULT_MODE = 'add'
SCOPES = ["https://www.googleapis.com/auth/script.projects"]

@deconstructible
class GoogleDriveStorage(Storage):
    CHUNK_SIZE = 4 * 1024 * 1024
    MAX_FILE_NAME_LENGTH = 30
    def __init__(self):
        self.OVERWRITE_FILE = setting('OVERWRITE_FILE', False)
        self.CLOUD_STORAGE_CREATE_NEW_IF_SAME_CONTENT = setting('CLOUD_STORAGE_CREATE_NEW_IF_SAME_CONTENT', True)
        self.GOOGLE_DRIVE_CREDENTIALS_FILE = setting('GOOGLE_DRIVE_CREDENTIALS_FILE')
        self.GOOGLE_DRIVE_TOKEN_FILE = setting('GOOGLE_DRIVE_TOKEN_FILE')
        self.GOOGLE_DRIVE_ROOT_FOLDER_ID = setting('GOOGLE_DRIVE_ROOT_FOLDER_ID')
        self.MEDIA_URL = setting('MEDIA_URL')
        self.timeout = setting('GOOGLE_DRIVE_TIMEOUT', _DEFAULT_TIMEOUT)

        # Initialize Google Drive service
        self.service = self._authenticate()
        
    
    def _authenticate(self):
        """Authenticate and build the Google Drive service"""
        creds = None
        
        # Load credentials from token file
        if os.path.exists(self.GOOGLE_DRIVE_TOKEN_FILE):
            creds = Credentials.from_authorized_user_file(self.GOOGLE_DRIVE_TOKEN_FILE)
        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                creds.refresh(Request())
            else:
                flow = InstalledAppFlow.from_client_secrets_file(self.GOOGLE_DRIVE_CREDENTIALS_FILE, SCOPES)
                creds = flow.run_local_server(port=0)
            # Save the credentials for the next run
            with open(self.GOOGLE_DRIVE_TOKEN_FILE, "w") as token:
                token.write(creds.to_json())
       
        return build('drive', 'v3', credentials=creds)
    


    def _get_folder_id(self, path):
        """Get or create folder ID for the given path"""
        if not path or path == '/':
            return self.GOOGLE_DRIVE_ROOT_FOLDER_ID
        
        folders = path.strip('/').split('/')
        current_folder_id = self.GOOGLE_DRIVE_ROOT_FOLDER_ID
        
        for folder_name in folders:
            if not folder_name:
                continue
                
            # Check if folder exists
            query = f"'{current_folder_id}' in parents and name='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
            results = self.service.files().list(
                q=query,
                spaces='drive',
                fields='files(id, name)'
            ).execute()
            items = results.get('files', [])
            
            if items:
                # Folder exists
                current_folder_id = items[0]['id']
            else:
                # Create new folder
                file_metadata = {
                    'name': folder_name,
                    'mimeType': 'application/vnd.google-apps.folder',
                    'parents': [current_folder_id]
                }
                folder = self.service.files().create(body=file_metadata, fields='id').execute()
                current_folder_id = folder.get('id')
        
        return current_folder_id


    def _get_file_id(self, name):
        """Get file ID by name and path"""
        if not name:
            return None
        
        # Extract directory and filename
        dir_name = os.path.dirname(name)
        file_name = os.path.basename(name)
        
        parent_id = self._get_folder_id(dir_name)
        
        query = f"'{parent_id}' in parents and name='{file_name}' and trashed=false"
        results = self.service.files().list(
            q=query,
            spaces='drive',
            fields='files(id, name, mimeType, size, modifiedTime, createdTime)'
        ).execute()
        items = results.get('files', [])
        
        return items[0]['id'] if items else None
    

    def _full_path(self, name):
        """Convert Django file path to proper format"""
        if name == '/':
            name = ''
        if name and name[0] in ["/", "\\"]:
            name = name[1:]
        return name


    def open(self, name, mode="rb"):
        """Retrieve the specified file from storage."""
        return self._open(name, mode)
    
    def _open(self, name, mode='rb'):
        full_name = self._full_path(name)
        file_id = self._get_file_id(full_name)
        if not file_id:
            e = Exception(f"File {name} not found in Google Drive")
            raise ContentDoesNotExistsError(e)
        
        # Download file content
        request = self.service.files().get_media(fileId=file_id)
        file_content = io.BytesIO()
        downloader = MediaIoBaseDownload(file_content, request)
        done = False
        while done is False:
            status, done = downloader.next_chunk()
        
        # Reset buffer position
        file_content.seek(0)
        
        # Create Django File object
        response_file = File(file_content, name=name)
        return response_file
    
    def save(self, name, content, max_length=None):
        """
        Save new content to the file specified by name. The content should be
        a proper File object or any Python file-like object, ready to be read
        from the beginning.
        """
        if name is None:
            name = content.name
        
        path = self.get_available_name(name, content, max_length=max_length)
        if path[1] is None:
            self._save(path[0], content)
        return path[0]  #Don't save the file, a file with same content exists
    
    def _save(self, name, content):
        full_name = self._full_path(name)
        
        # Extract directory and filename
        dir_name = os.path.dirname(full_name)
        file_name = os.path.basename(full_name)
        
        parent_id = self._get_folder_id(dir_name)
        
        content.open()
        file_metadata = {
            'name': file_name,
            'parents': [parent_id]
        }
        
        try:
            # Check if file exists for update
            existing_file_id = self._get_file_id(full_name)
            
            if existing_file_id:
                # Update existing file
                media = MediaIoBaseUpload(
                    content,
                    mimetype=getattr(content, 'content_type', 'application/octet-stream'),
                    resumable=True
                )
                updated_file = self.service.files().update(
                    fileId=existing_file_id,
                    media_body=media,
                    fields='id'
                ).execute()
            else:
                # Create new file
                media = MediaIoBaseUpload(
                    content,
                    mimetype=getattr(content, 'content_type', 'application/octet-stream'),
                    resumable=True
                )
                uploaded_file = self.service.files().create(
                    body=file_metadata,
                    media_body=media,
                    fields='id'
                ).execute()
            
            content.seek(0)
            return name
            
        except HttpError as e:
            throwGoogleDriveException(e)


    def get_available_name(self, name, content, max_length=None):
        """
        Return a filename that's free on the target storage system and
        available for new content to be written to.
        """
        formatted_name = self.get_valid_name(name)
        new_name = formatted_name
        if self.OVERWRITE_FILE:
            try:
                self.delete(new_name)
            except ContentDoesNotExistsError:
                pass
        else:
            index = 0
            while(1):
                index += 1
                if self.exists(new_name): # check file existence with file name
                    if not self.CLOUD_STORAGE_CREATE_NEW_IF_SAME_CONTENT:
                        # check file existence with file's contents
                        remote_file = self.open(new_name)
                        remote_file_data = remote_file.read()
                        content_data = content.read()
                        if remote_file_data == content_data:
                            return (new_name, 'Exists')
                    new_name = self.get_alternative_name(formatted_name, index=index)
                    continue
                break
        return (new_name, None)
    
    def generate_filename(self, filename):
        """
        Validate the filename by calling get_valid_name() and return a filename
        to be passed to the save() method.
        """
        name = self.get_valid_name(filename)
        return name

    def get_valid_name(self, name):
        """
        Return a filename, based on the provided filename, that's suitable for
        use in the target storage system.
        """
        name = str(name).replace("\\", "/")
        return name

    def get_alternative_name(self, file_root, file_ext=None, index=0):
        """
        Return an alternative filename if one exists to the filename.
        """
        res = file_root.rsplit('.', 1)
        if len(res) == 2:
            file_name = f"{res[0]}({index})"
            file_ext = res[1]
            updated_name = f"{file_name}.{file_ext}"
        else:
            updated_name = f"{file_root}({index})"
        return updated_name

    def delete(self, name):
        """
        Delete the specified file from the storage system.
        """
        full_name = self._full_path(name)
        try:
            file_id = self._get_file_id(full_name)
            if file_id:
                self.service.files().delete(fileId=file_id).execute()
            else:
                raise ContentDoesNotExistsError(f"File {name} not found")
        except Exception as e:
            throwGoogleDriveException(e)

    def exists(self, name):
        """
        Return True if a file referenced by the given name already exists in the
        storage system, or False if the name is available for a new file.
        """
        full_name = self._full_path(name)
        file_id = self._get_file_id(full_name)
        return file_id is not None
        
    def listdir(self, path):
        """
        List the contents of the specified path. Return a 2-tuple of lists:
        the first item being directories, the second item being files.
        """
        directories, files = [], []
        full_path = self._full_path(path)
        folder_id = self._get_folder_id(full_path)
        
        try:
            # Query for folders
            folder_query = f"'{folder_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"
            folder_results = self.service.files().list(
                q=folder_query,
                spaces='drive',
                fields='files(name)'
            ).execute()
            directories = [folder['name'] for folder in folder_results.get('files', [])]
            
            # Query for files (excluding folders)
            file_query = f"'{folder_id}' in parents and mimeType!='application/vnd.google-apps.folder' and trashed=false"
            file_results = self.service.files().list(
                q=file_query,
                spaces='drive',
                fields='files(name)'
            ).execute()
            files = [file['name'] for file in file_results.get('files', [])]
            
            return directories, files
            
        except HttpError as e:
            throwGoogleDriveException(e)
    
    def size(self, name):
        """
        Return the total size, in bytes, of the file specified by name.
        """
        full_name = self._full_path(name)
        try:
            file_id = self._get_file_id(full_name)
            if file_id:
                file_metadata = self.service.files().get(
                    fileId=file_id, 
                    fields='size'
                ).execute()
                return int(file_metadata.get('size', 0))
            return 0
        except HttpError as e:
            throwGoogleDriveException(e)

    
    def url(self, name, permanent_link=None):
        """
        Return an absolute URL where the file's contents can be accessed directly by a web browser.
        """
        full_name = self._full_path(name)
        try:
            file_id = self._get_file_id(full_name)
            if file_id:
                # Create a public permission (readable by anyone with the link)
                permission = {
                    'type': 'anyone',
                    'role': 'reader'
                }
                self.service.permissions().create(
                    fileId=file_id,
                    body=permission
                ).execute()
                
                # Get the webViewLink
                file_metadata = self.service.files().get(
                    fileId=file_id,
                    fields='webViewLink, webContentLink'
                ).execute()
                
                return file_metadata.get('webContentLink') or file_metadata.get('webViewLink')
            return None
        except HttpError as e:
            throwGoogleDriveException(e)
    

    def get_accessed_time(self, name):
        """
        Return the last accessed time (as a datetime) of the file specified by name.
        The datetime will be timezone-aware if USE_TZ=True.
        """
        # Google Drive doesn't track last accessed time, so we return modified time
        return self.get_modified_time(name)

    def get_created_time(self, name):
        """
        Return the creation time (as a datetime) of the file specified by name.
        The datetime will be timezone-aware if USE_TZ=True.
        """
        full_name = self._full_path(name)
        try:
            file_id = self._get_file_id(full_name)
            if file_id:
                file_metadata = self.service.files().get(
                    fileId=file_id,
                    fields='createdTime'
                ).execute()
                from datetime import datetime
                return datetime.fromisoformat(file_metadata['createdTime'].replace('Z', '+00:00'))
            return None
        except HttpError as e:
            throwGoogleDriveException(e)
    
    def get_modified_time(self, name):
        """
        Return the last modified time (as a datetime) of the file specified by
        name. The datetime will be timezone-aware if USE_TZ=True.
        """
        full_name = self._full_path(name)
        try:
            file_id = self._get_file_id(full_name)
            if file_id:
                file_metadata = self.service.files().get(
                    fileId=file_id,
                    fields='modifiedTime'
                ).execute()
                from datetime import datetime
                return datetime.fromisoformat(file_metadata['modifiedTime'].replace('Z', '+00:00'))
            return None
        except HttpError as e:
            throwGoogleDriveException(e)