"""Upload workflow task for Office 365"""

import mimetypes
import tempfile
from collections import OrderedDict
from collections.abc import Sequence
from pathlib import Path

from cmem.cmempy.workspace.projects.resources.resource import get_resource_response
from cmem_plugin_base.dataintegration.context import ExecutionContext, ExecutionReport
from cmem_plugin_base.dataintegration.description import Icon, Plugin, PluginParameter
from cmem_plugin_base.dataintegration.entity import Entities
from cmem_plugin_base.dataintegration.parameter.choice import ChoiceParameterType
from cmem_plugin_base.dataintegration.parameter.password import Password, PasswordParameterType
from cmem_plugin_base.dataintegration.parameter.resource import ResourceParameterType
from cmem_plugin_base.dataintegration.plugins import WorkflowPlugin
from cmem_plugin_base.dataintegration.ports import FixedNumberOfInputs, FixedSchemaPort, InputPorts
from cmem_plugin_base.dataintegration.typed_entities.file import (
    File,
    FileEntitySchema,
    LocalFile,
    ProjectFile,
)
from cmem_plugin_base.dataintegration.utils import setup_cmempy_user_access
from office365.graph_client import GraphClient
from office365.runtime.client_object import ClientObject

from cmem_plugin_office365.autocompletion import (
    DriveDirectoryParameterType,
    FolderDirectoryParameterType,
    ResourceDirectoryParameterType,
)

RESOURCE_CHOICE = OrderedDict({"personal": "Personal", "site": "Site"})
MAX_WORKERS = 32


@Plugin(
    label="Office 365 Upload Files",
    description="Upload files to OneDrive or a site Sharepoint",
    documentation="""
This workflow task upload files to specified Office 365 instance.
For this to work a registered app in Microsoft's Entra ID space is necessary.
Further information can be found [here](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app).

After registering an application, it needs to be granted application wide API permissions:
- Files.Read.All, Files.Write.All
- Sites.Read.All, Sites.Write.All

Admin consent is required to activate these permissions.
With this setup, anyone with the secret can access all users' OneDrives and all Sharepoint/Team
sites.

#### Important
Make sure only trusted admins can create or manage secrets!
Whoever holds the secrets has all the access to granted resources so best not to distribute
recklessly.
    """,
    icon=Icon(package=__package__, file_name="o365-upload.svg"),
    parameters=[
        PluginParameter(
            name="tenant_id",
            label="Tenant ID",
            description="ID of your tenant. Can be seen within your registered application",
        ),
        PluginParameter(
            name="client_id",
            label="Client ID",
            description="Client ID of your registered application.",
        ),
        PluginParameter(
            param_type=PasswordParameterType(),
            name="client_secret",
            label="Client secret",
            description="Client secret created withing your registered application.",
        ),
        PluginParameter(
            param_type=ChoiceParameterType(OrderedDict({"personal": "Personal", "site": "Site"})),
            name="type_resource",
            label="Type resource",
            description="The type of resource you want the data to be extracted from. "
            "This can either be a site or a users share",
        ),
        PluginParameter(
            name="target_resource",
            label="Target resource",
            description="Target resource which files will be listed from. This can either be a "
            "specific users share address or a microsoft site URL.",
            param_type=ResourceDirectoryParameterType("resource", "Resource"),
        ),
        PluginParameter(
            name="drives",
            label="Drives",
            description="A list of drives from the selected target resource.",
            param_type=DriveDirectoryParameterType("drives", "Drives"),
        ),
        PluginParameter(
            name="path",
            label="Directory path",
            description="The path of a directory that needs to be transformed. Includes all "
            "subdirectories by default",
            param_type=FolderDirectoryParameterType("path", "Path"),
        ),
        PluginParameter(
            name="source_file",
            label="File",
            description="The file which you would like to upload. If this is set, no connections "
            "in the workflow are allowed",
            param_type=ResourceParameterType(),
            advanced=True,
            default_value="",
        ),
        PluginParameter(
            name="max_workers",
            label="Maximum amount of workers",
            description="Specifies the maximum number of threads used for parallel execution of "
            "the workflow. "
            "The default is 32, and the valid range is 1 to 32. "
            "Note: Due to known throttling limits imposed by Microsoft, running with high "
            "parallelism may cause errors. "
            "If you encounter issues, try reducing the number of threads to 1.",
            advanced=True,
            default_value=32,
        ),
    ],
)
class UploadPlugin(WorkflowPlugin):
    """Upload plugin Office 365"""

    def __init__(  # noqa: PLR0913
        self,
        tenant_id: str,
        client_id: str,
        client_secret: Password | str,
        type_resource: str,
        target_resource: str,
        drives: str,
        source_file: str,
        max_workers: int = 32,
        path: str = "",
    ) -> None:
        self.tenant_id = tenant_id
        self.client_id = client_id
        self.client_secret = (
            client_secret if isinstance(client_secret, str) else client_secret.decrypt()
        )
        self.path = path
        self.type_resource = RESOURCE_CHOICE[type_resource]
        self.target_resource = target_resource
        self.drives = drives
        self.source_file = source_file
        self.max_workers = setup_max_workers(max_workers)
        self.client = GraphClient(tenant=tenant_id).with_client_secret(
            self.client_id, self.client_secret
        )
        self.drive = self.create_drive()
        self.input_ports = self.initialise_input_ports()
        self.output_port = None

    def initialise_input_ports(self) -> InputPorts:
        """Initialise the input ports depending on weather a source file is given or not"""
        return (
            FixedNumberOfInputs([FixedSchemaPort(schema=FileEntitySchema())])
            if self.source_file == ""
            else FixedNumberOfInputs([])
        )

    def create_drive(self) -> ClientObject:
        """Create drive"""
        s = self.client.sites.get_by_url(self.target_resource)
        return s.drive

    def execute(self, inputs: Sequence[Entities], context: ExecutionContext) -> Entities:
        """Execute the workflow task"""
        schema = FileEntitySchema()
        files: list[File] = []

        if len(inputs) == 0:
            self.upload_no_input(context, files)
        else:
            self.upload_with_input(context, files, inputs)

        entities = [schema.to_entity(file) for file in files]

        context.report.update(
            ExecutionReport(
                entity_count=len(entities),
                operation="write",
                operation_desc="files uploaded",
                sample_entities=Entities(entities=iter(entities[:10]), schema=schema),
            )
        )

        return Entities(entities=iter(entities), schema=schema)

    def upload_no_input(self, context: ExecutionContext, files: list[File]) -> None:
        """Upload of a source file with no input given"""
        if self.source_file != "":
            setup_cmempy_user_access(context.user)
            file = get_resource_response(context.task.project_id(), self.source_file)
            mime_type, _ = mimetypes.guess_type(self.source_file)
            context.report.update(
                ExecutionReport(
                    entity_count=len(files),
                    operation="upload",
                    operation_desc=f"uploading {self.source_file}",
                )
            )
            with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
                tmp_file.write(file.content)
                tmp_file.seek(0)
                with Path(tmp_file.name).open("rb") as f:
                    self.drive.root.upload_file(f).execute_query()

            files.append(LocalFile(path=str(self.source_file), mime=mime_type))

    def upload_with_input(
        self, context: ExecutionContext, files: list[File], inputs: Sequence[Entities]
    ) -> None:
        """Upload of files from a given input"""
        schema = FileEntitySchema()
        for entity in inputs[0].entities:
            try:
                if context.workflow.status() == "Canceling":
                    break
            except AttributeError:
                pass

            entity_file = schema.from_entity(entity)
            file_path = Path(entity_file.path)

            context.report.update(
                ExecutionReport(
                    entity_count=len(files),
                    operation="upload",
                    operation_desc=f"uploading {file_path}",
                )
            )

            if entity_file.file_type != "Local":
                setup_cmempy_user_access(context.user)
                file = get_resource_response(context.task.project_id(), str(file_path))
                with tempfile.TemporaryDirectory() as tmp_dir:
                    tmp_file_path = Path(tmp_dir) / file_path.name
                    tmp_file_path.write_bytes(file.content)
                    with tmp_file_path.open("rb") as f:
                        if self.path == "":
                            self.drive.root.upload_file(f).execute_query()
                        else:
                            self.path = _strip_path(self.path)
                            self.drive.root.get_by_path(self.path).upload_file(f).execute_query()

            elif self.path == "":
                self.drive.root.upload_file(file_path).execute_query()
            else:
                self.path = _strip_path(self.path)
                self.drive.root.get_by_path(self.path).upload_file(file_path).execute_query()

            if entity_file.file_type == "Local":
                files.append(LocalFile(path=str(file_path), mime=entity_file.mime))
            elif entity_file.file_type == "Project":
                files.append(ProjectFile(path=str(file_path), mime=entity_file.mime))


def setup_max_workers(max_workers: int) -> int:
    """Return the correct number of workers"""
    if 0 < max_workers <= MAX_WORKERS:
        return max_workers
    raise ValueError("Range of max_workers exceeded")


def _strip_path(path: str) -> str:
    """Strip path of trailing '/'"""
    if path[-1] == "/":
        path = path[:-1]
    return path
