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.errors import HttpError
from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload

from langchain_core.tools import tool
import streamlit as st
import os
import io

SCOPES = [
    'https://www.googleapis.com/auth/drive',
    'https://www.googleapis.com/auth/drive.file'
]

@st.cache_resource
def get_google_drive_service():
  """
  Gets the Google Drive credentials with the scope of full access to Drive files
  """
  creds = None
  if os.path.exists("token.json"):
    creds = Credentials.from_authorized_user_file("token.json", SCOPES)
  # If there are no (valid) credentials available, let the user log in.
  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(
          "credentials/credentials.json", SCOPES
      )
      creds = flow.run_local_server(port=0)
    # Save the credentials for the next run
    with open("token.json", "w") as token:
      token.write(creds.to_json())

  return build("drive", "v3", credentials=creds)

service = get_google_drive_service()

@tool
def search_file(query: str) -> list:
    """
    Searches for files in Google Drive based on a query string.
    
    Arguments:
    - query (str): The search query to find files. This requires a specific format for Google Drive:
    To search for files that have 'example' in the name - query should be: name contains 'example'
    To search for files that have 'example text' in the file text - query should be: fullText contains 'example text'
    
    Returns:
    - list: A list of dictionaries containing the file ID and name of the matched files.
    
    Example usage:
    search_file("name contains 'report'")
    """
    try:
        results = service.files().list(q=f"mimeType!='application/vnd.google-apps.folder' and {query}", spaces='drive', fields="files(id, name)").execute()
        return str(results.get('files', []))
    except Exception as e:
        return f"Failed to search Google Drive: {e}"

@tool
def download_file(file_id: str, file_name: str, mime_type: str = 'text/plain') -> str:
    """
    Downloads a Google Docs file (or similar) from Google Drive and saves it to a specified path.
    
    Arguments:
    - file_id (str): The unique ID of the file to be downloaded.
    - file_name (str): The name of the file (including the extension) to download it locally as.
    - mime_type (str, optional): The MIME type to export the file as. Defaults to 'text/plain'.
    
    Returns:
    - str: A message confirming the file has been downloaded to the specified path.
    
    Example usage:
    download_file("1aBcDeFgHiJkLmNoPqRsTuVwXyZ", "file.txt", "text/plain")
    """
    try:
        directory = "data"
        if not os.path.exists(directory):
            os.makedirs(directory, exist_ok=True)

        request = service.files().export_media(fileId=file_id, mimeType=mime_type)
        file_path = f"{directory}/{file_name}"
        with io.FileIO(file_path, 'wb') as file:
            downloader = MediaIoBaseDownload(file, request)
            done = False
            while not done:
                status, done = downloader.next_chunk()
        
        return f"File downloaded to {file_path}"
    except Exception as e:
        return f"Error downloading the file: {e}"


@tool
def upload_file(file_path: str, folder_id: str = None) -> str:
    """
    Uploads a file to a specific folder in Google Drive. If no folder ID is provided, it uploads to the root directory.
    
    Arguments:
    - file_path (str): The local path to the file that will be uploaded.
    - folder_id (str, optional): The ID of the Google Drive folder where the file will be uploaded. Defaults to None (uploads to root).
    
    Returns:
    - str: The ID of the uploaded file.
    
    Example usage:
    upload_file("/path/to/local/file.txt", "1aBcDeFgHiJkLmNoPqRsTuVwXyZ")
    """
    try:
        file_metadata = {'name': file_path.split("/")[-1]}
        if folder_id:
            file_metadata['parents'] = [folder_id]
        media = MediaFileUpload(file_path, resumable=True)
        file = service.files().create(body=file_metadata, media_body=media, fields='id').execute()
        return f"File uploaded with ID: {file.get('id')}"
    except Exception as e:
        return f"Error uploading the file: {e}"

@tool
def delete_file(file_id: str) -> str:
    """
    Deletes a file from Google Drive based on its file ID.
    
    Arguments:
    - file_id (str): The unique ID of the file to be deleted.
    
    Returns:
    - str: A message confirming the deletion of the file.
    
    Example usage:
    delete_file("1aBcDeFgHiJkLmNoPqRsTuVwXyZ")
    """
    try:
        service.files().delete(fileId=file_id).execute()
        return f"File with ID {file_id} has been deleted."
    except Exception as e:
        return f"Error deleting the file: {e}"

@tool
def update_file(file_id: str, new_file_path: str) -> str:
    """
    Updates the contents of a file in Google Drive by replacing it with a new file.
    
    Arguments:
    - file_id (str): The unique ID of the file to be updated.
    - new_file_path (str): The local path to the new file that will replace the existing file.
    
    Returns:
    - str: A message confirming the file has been updated.
    
    Example usage:
    update_file("1aBcDeFgHiJkLmNoPqRsTuVwXyZ", "/path/to/new/file.txt")
    """
    try:
        media = MediaFileUpload(new_file_path, resumable=True)
        updated_file = service.files().update(fileId=file_id, media_body=media).execute()
        return f"File with ID {file_id} has been updated."
    except Exception as e:
        return f"Error updating the file: {e}"

@tool
def search_folder(query: str) -> list:
    """
    Searches for folders in Google Drive based on a query string.
    
    Arguments:
    - query (str): The search query to find folders - just the name or part of the name of folder(s) to search for.
    
    Returns:
    - list: A list of dictionaries containing the folder ID and name of the matched folders.
    
    Example usage:
    search_folder("name contains 'meeting_notes'")
    """
    try:
        results = service.files().list(q=f"mimeType='application/vnd.google-apps.folder' and name contains '{query}'",
                                    spaces='drive', fields="files(id, name)").execute()
        return str(results.get('files', []))
    except Exception as e:
        return f"Error searching folders: {e}"

@tool
def create_folder(folder_name: str, parent_folder_id: str = None) -> str:
    """
    Creates a folder in Google Drive. If a parent folder ID is provided, the folder is created inside that folder.
    
    Arguments:
    - folder_name (str): The name of the folder to be created.
    - parent_folder_id (str, optional): The ID of the parent folder where the new folder will be created. Defaults to None (creates in root).
    
    Returns:
    - str: The ID of the created folder.
    
    Example usage:
    create_folder("New Meeting Folder", "1aBcDeFgHiJkLmNoPqRsTuVwXyZ")
    """
    try:
        file_metadata = {
            'name': folder_name,
            'mimeType': 'application/vnd.google-apps.folder'
        }
        if parent_folder_id:
            file_metadata['parents'] = [parent_folder_id]
        folder = service.files().create(body=file_metadata, fields='id').execute()
        return f"Folder created with ID: {folder.get('id')}"
    except Exception as e:
        return f"Error creating the folder: {e}"

@tool
def delete_folder(folder_id: str) -> str:
    """
    Deletes a folder from Google Drive based on its folder ID.
    
    Arguments:
    - folder_id (str): The unique ID of the folder to be deleted.
    
    Returns:
    - str: A message confirming the deletion of the folder.
    
    Example usage:
    delete_folder("1aBcDeFgHiJkLmNoPqRsTuVwXyZ")
    """
    try:
        service.files().delete(fileId=folder_id).execute()
        return f"Folder with ID {folder_id} has been deleted."
    except Exception as e:
        return f"Error deleting the folder: {e}"

@tool
def create_text_file(content: str, file_name: str) -> str:
    """
    Creates a text file with the given content + file name and returns the file path.
    
    Arguments:
    - content (str): The text content to be written to the file.
    - file_name (str): The name of the file to be created (including the file extension, typically .txt).
    
    Returns:
    - str: The path to the created text file.
    
    Example usage:
    create_text_file("Hello, world!", "example.txt")
    """
    try:
        directory = "data"
        if not os.path.exists(directory):
            os.makedirs(directory, exist_ok=True)

        file_path = f"{directory}/{file_name}"
        with open(file_path, "w") as file:
            file.write(content)
        return file_path
    except Exception as e:
        return f"Error creating the text file: {e}"

# Maps the function names to the actual function object in the script
# This mapping will also be used to create the list of tools to bind to the agent
available_drive_functions = {
  "search_file": search_file,
  "download_file": download_file,
  "upload_file": upload_file,
  "delete_file": delete_file,
  "update_file": update_file,
  "search_folder": search_folder,
  "create_folder": create_folder,
  "delete_folder": delete_folder,
  "create_text_file": create_text_file
}
