import pandas as pd
from pydantic import BaseModel
from typing import Dict, List, Literal, Optional
from lexsi_sdk.client.client import APIClient
from lexsi_sdk.common.utils import normalize_time
from lexsi_sdk.common.validation import Validate
from lexsi_sdk.common.xai_uris import (
    AVAILABLE_CUSTOM_SERVERS_URI,
    CREATE_WORKSPACE_URI,
    GET_WORKSPACES_URI,
    INVITE_USER_ORGANIZATION_URI,
    ORGANIZATION_MEMBERS_URI,
    REMOVE_USER_ORGANIZATION_URI,
    UPDATE_ORGANIZATION_URI,
)
from lexsi_sdk.core.workspace import Workspace
from lexsi_sdk.common.types import CustomServerConfig, GCSConfig, S3Config, GDriveConfig, SFTPConfig
from lexsi_sdk.common.xai_uris import (
    AVAILABLE_CUSTOM_SERVERS_URI,
    CREATE_DATA_CONNECTORS,
    LIST_DATA_CONNECTORS,
    DELETE_DATA_CONNECTORS,
    TEST_DATA_CONNECTORS,
    DROPBOX_OAUTH,
    LIST_BUCKETS,
    LIST_FILEPATHS,
    COMPUTE_CREDIT_URI,
)
from lexsi_sdk.core.utils import build_url, build_list_data_connector_url


class Organization(BaseModel):
    """Represents a Lexsi organization. Provides APIs to manage workspaces, users, data connectors, and organization-scoped resources."""

    organization_id: Optional[str] = None
    name: str
    created_by: str
    created_at: Optional[str] = None

    api_client: APIClient

    def __init__(self, **kwargs):
        """Attach API client to the organization instance.
        Stores configuration and prepares the object for use."""
        super().__init__(**kwargs)
        self.api_client = kwargs.get("api_client")

    def add_user_to_organization(self, user_email: str) -> str:
        """Invite a user to join the organization by sending an invitation email. Requires the user’s email address and uses the organization ID internally to associate the user.

        :param user_email: Email of user to be added to organization.
        :return: response
        """
        payload = {
            "email": user_email,
            "organization_id": self.organization_id,
        }
        res = self.api_client.post(INVITE_USER_ORGANIZATION_URI, payload)

        if not res["success"]:
            raise Exception(res.get("details", "Failed to add user to organization"))

        return res.get("details", "User added successfully")

    def remove_user_from_organization(self, user_email: str) -> str:
        """Remove an existing user from the organization using their email address. Returns a confirmation message on success.

        :param user_email: Email of user to be removed from organization.
        :return: response
        """
        payload = {
            "organization_user_email": user_email,
            "organization_id": self.organization_id,
        }
        res = self.api_client.post(REMOVE_USER_ORGANIZATION_URI, payload)

        if not res["success"]:
            raise Exception(
                res.get("details", "Failed to remove user from organization")
            )

        return res.get("details", "User removed successfully")

    def member_details(self) -> pd.DataFrame:
        """Return a DataFrame containing details about members of the organization, including their names, emails, roles (owner/admin), and creation dates.

        :return: member details dataframe
        """
        res = self.api_client.get(
            f"{ORGANIZATION_MEMBERS_URI}?organization_id={self.organization_id}"
        )

        if not res["success"]:
            raise Exception(
                res.get("details", "Failed to get organization member details")
            )

        member_details_df = pd.DataFrame(
            res.get("details").get("users"),
            columns=[
                "full_name",
                "email",
                "organization_owner",
                "organization_admin",
                "created_at",
            ],
        )

        return member_details_df

    def workspaces(self) -> pd.DataFrame:
        """List all workspaces associated with the organization. Returns a DataFrame with workspace names, access types, creator, and instance details.

        :return: workspace details dataframe
        """

        url = GET_WORKSPACES_URI
        if self.organization_id:
            url = url + f"?organization_id={self.organization_id}"
        workspaces = self.api_client.get(url)

        workspace_df = pd.DataFrame(
            workspaces["details"],
            columns=[
                "user_workspace_name",
                "access_type",
                "created_by",
                "created_at",
                "updated_at",
                "instance_type",
                "instance_status",
            ],
        )

        return workspace_df

    def workspace(self, workspace_name: str) -> Workspace:
        """Select a specific workspace by name within the organization and return a Workspace object for further operations.

        :param workspace_name: Name of the workspace to be used
        :return: Workspace
        """

        url = GET_WORKSPACES_URI
        if self.organization_id:
            url = url + f"?organization_id={self.organization_id}"
        workspaces = self.api_client.get(url)
        user_workspaces = [
            Workspace(api_client=self.api_client, **workspace)
            for workspace in workspaces["details"]
        ]

        workspace = next(
            filter(
                lambda workspace: workspace.user_workspace_name == workspace_name,
                user_workspaces,
            ),
            None,
        )

        if workspace is None:
            raise Exception("Workspace Not Found")

        return workspace

    def create_workspace(
        self, workspace_name: str, server_type: Optional[str] = None, server_config: Optional[CustomServerConfig] = CustomServerConfig()
    ) -> Workspace:
        """Create a new workspace within the organization. Accepts a workspace name and an optional server_type to specify the compute instance. Returns a Workspace object for the newly created workspace.

        :param workspace_name: name for the workspace
        :param server_type: dedicated instance to run workloads
            for all available instances check lexsi.available_node_servers()
            defaults to local
        :param server_config: workspace server settings
        {
            "compute_type": "2xlargeA10G",  # compute_type for workspace
            "custom_server_config": {
                "start": "14:00+05:30" or "14:00",  # Start time ("HH:MM±HH:MM" or "HH:MM"; assumed UTC if no offset)
                "stop": "15:00+05:30" or "15:00"",  # Stop time ("HH:MM±HH:MM" or "HH:MM"; assumed UTC if no offset)
                "shutdown_after": 5,  # Operation hours for custom server
                "op_hours": True / False  # Whether to restrict to business hours
                "auto_start": True / False  # Automatically start the server when requested.
            }
        }
        :return: response
        """
        payload = {"workspace_name": workspace_name}

        if server_type:
            custom_servers = self.api_client.get(AVAILABLE_CUSTOM_SERVERS_URI)
            Validate.value_against_list(
                "server_type",
                server_type,
                [server["name"] for server in custom_servers],
            )

            payload["instance_type"] = server_type
            if server_config and server_config.get("start", None) and server_config.get("stop", None):
                server_config["start"] = normalize_time(server_config.get("start"))
                server_config["stop"] = normalize_time(server_config.get("stop"))
            payload["server_config"] = server_config if server_config else {}

        if self.organization_id:
            payload["organization_id"] = self.organization_id

        res = self.api_client.post(CREATE_WORKSPACE_URI, payload)

        if not res["success"]:
            raise Exception(res.get("details"))

        workspace = Workspace(api_client=self.api_client, **res["workspace_details"])

        return workspace

    def __print__(self) -> str:
        """User-friendly string representation.
        Encapsulates a small unit of SDK logic and returns the computed result."""
        return f"Organization(name='{self.name}', created_by='{self.created_by}', created_at='{self.created_at}')"

    def __str__(self) -> str:
        """Return printable representation.
        Summarizes the instance in a concise form."""
        return self.__print__()

    def __repr__(self) -> str:
        """Return developer-friendly representation.
        Includes key fields useful for logging and troubleshooting."""
        return self.__print__()

    def create_data_connectors(
        self,
        data_connector_name: str,
        data_connector_type: str,
        gcs_config: Optional[GCSConfig] = None,
        s3_config: Optional[S3Config] = None,
        gdrive_config: Optional[GDriveConfig] = None,
        sftp_config: Optional[SFTPConfig] = None,
        hf_token: Optional[str] = None,
    ) -> str:
        """Create a data connector for a organization, allowing external data (e.g., S3, GCS, Google Drive, SFTP, Dropbox, HuggingFace) to be linked. Requires the connector name and type, plus the corresponding credential dictionary depending on the connector type.
        For Dropbox, an authentication link will be generated during execution, and user authorization code is required to complete setup.

        :param data_connector_name: name for data connector
        :param data_connector_type: type of data connector (s3 | gcs | gdrive | dropbox | sftp | HuggingFace)
        :param gcs_config: credentials from service account json
        :param s3_config: credentials of s3 storage
        :param gdrive_config: credentials from service account json
        :param sftp_config: hostname, port, username and password for sftp connection
        :return: response
        """
        if not self.organization_id:
            return "No Organization id found"
        if data_connector_type.lower() == "s3":
            if not s3_config:
                return "No configuration for S3 found"

            Validate.value_against_list(
                "s3 config",
                list(s3_config.keys()),
                ["region", "access_key", "secret_key"],
            )

            payload = {
                "link_service": {
                    "service_name": data_connector_name,
                    "region": s3_config.get("region", "ap-south-1"),
                    "access_key": s3_config.get("access_key"),
                    "secret_key": s3_config.get("secret_key"),
                },
                "link_service_type": data_connector_type,
            }

        if data_connector_type.lower() == "gcs":
            if not gcs_config:
                return "No configuration for GCS found"

            Validate.value_against_list(
                "gcs config",
                list(gcs_config.keys()),
                [
                    "project_id",
                    "gcp_project_name",
                    "type",
                    "private_key_id",
                    "private_key",
                    "client_email",
                    "client_id",
                    "auth_uri",
                    "token_uri",
                ],
            )

            payload = {
                "link_service": {
                    "service_name": data_connector_name,
                    "project_id": gcs_config.get("project_id"),
                    "gcp_project_name": gcs_config.get("gcp_project_name"),
                    "service_account_json": {
                        "type": gcs_config.get("type"),
                        "project_id": gcs_config.get("project_id"),
                        "private_key_id": gcs_config.get("private_key_id"),
                        "private_key": gcs_config.get("private_key"),
                        "client_email": gcs_config.get("client_email"),
                        "client_id": gcs_config.get("client_id"),
                        "auth_uri": gcs_config.get("auth_uri"),
                        "token_uri": gcs_config.get("token_uri"),
                    },
                },
                "link_service_type": data_connector_type,
            }

        if data_connector_type == "gdrive":
            if not gdrive_config:
                return "No configuration for Google Drive found"

            Validate.value_against_list(
                "gdrive config",
                list(gdrive_config.keys()),
                [
                    "project_id",
                    "type",
                    "private_key_id",
                    "private_key",
                    "client_email",
                    "client_id",
                    "auth_uri",
                    "token_uri",
                ],
            )

            payload = {
                "link_service": {
                    "service_name": data_connector_name,
                    "service_account_json": {
                        "type": gdrive_config.get("type"),
                        "project_id": gdrive_config.get("project_id"),
                        "private_key_id": gdrive_config.get("private_key_id"),
                        "private_key": gdrive_config.get("private_key"),
                        "client_email": gdrive_config.get("client_email"),
                        "client_id": gdrive_config.get("client_id"),
                        "auth_uri": gdrive_config.get("auth_uri"),
                        "token_uri": gdrive_config.get("token_uri"),
                    },
                },
                "link_service_type": data_connector_type,
            }

        if data_connector_type == "sftp":
            if not sftp_config:
                return "No configuration for Google Drive found"

            Validate.value_against_list(
                "sftp config",
                list(sftp_config.keys()),
                ["hostname", "port", "username", "password"],
            )

            payload = {
                "link_service": {
                    "service_name": data_connector_name,
                    "sftp_json": {
                        "hostname": sftp_config.get("hostname"),
                        "port": sftp_config.get("port"),
                        "username": sftp_config.get("username"),
                        "password": sftp_config.get("password"),
                    },
                },
                "link_service_type": data_connector_type,
            }

        if data_connector_type == "dropbox":
            url_data = self.api_client.get(
                f"{DROPBOX_OAUTH}?organization_id={self.organization_id}"
            )
            print(f"Url: {url_data['details']['url']}")
            code = input(f"{url_data['details']['message']}: ")

            if not code:
                return "No authentication code provided."

            payload = {
                "link_service": {
                    "service_name": data_connector_name,
                    "dropbox_json": {"code": code},
                },
                "link_service_type": data_connector_type,
            }

        if data_connector_type == "HuggingFace":
            if not hf_token:
                return "No hf_token provided"

            payload = {
                "link_service": {
                    "service_name": data_connector_name,
                    "hf_token": hf_token,
                },
                "link_service_type": data_connector_type,
            }

        url = build_url(
            CREATE_DATA_CONNECTORS, data_connector_name, None, self.organization_id
        )
        res = self.api_client.post(url, payload)
        return res["details"]

    def test_data_connectors(self, data_connector_name: str) -> str:
        """Test the connection of an existing data connector to ensure credentials and connectivity are valid. Takes the connector name as input and returns the status of the connection test.

        :param data_connector_name: name of the data connector to be tested.
        """
        if not data_connector_name:
            return "Missing argument data_connector_name"
        if not self.organization_id:
            return "No Project Name or Organization id found"
        url = build_url(
            TEST_DATA_CONNECTORS, data_connector_name, None, self.organization_id
        )
        res = self.api_client.post(url)
        return res["details"]

    def delete_data_connectors(self, data_connector_name: str) -> str:
        """Delete a data connector from the organization using its name. This removes the external data link and returns a confirmation message.

        :param data_connector_name: name of the data connector to be deleted.
        """
        if not data_connector_name:
            return "Missing argument data_connector_name"
        if not self.organization_id:
            return "No Project Name or Organization id found"

        url = build_url(
            DELETE_DATA_CONNECTORS, data_connector_name, None, self.organization_id
        )
        res = self.api_client.post(url)
        return res["details"]

    def list_data_connectors(self) -> str | pd.DataFrame:
        """List all data connectors configured in the organization. If successful, returns a DataFrame with details about each connector; otherwise returns an error message."""
        url = build_list_data_connector_url(
            LIST_DATA_CONNECTORS, None, self.organization_id
        )
        res = self.api_client.post(url)

        if res["success"]:
            df = pd.DataFrame(res["details"])
            df = df.drop(
                [
                    "_id",
                    "region",
                    "gcp_project_name",
                    "gcp_project_id",
                    "gdrive_file_name",
                    "project_name",
                ],
                axis=1,
                errors="ignore",
            )
            return df

        return res["details"]

    def list_data_connectors_buckets(self, data_connector_name: str) -> str | List:
        """Retrieve the list of buckets (for S3 or GCS connectors) or similar container names for the specified data connector.

        :param data_connector_name: name of the data connector
        """
        if not data_connector_name:
            return "Missing argument data_connector_name"
        if not self.organization_id:
            return "No Organization id found"

        url = build_url(LIST_BUCKETS, data_connector_name, None, self.organization_id)
        res = self.api_client.get(url)

        if res.get("message", None):
            print(res["message"])
        return res["details"]

    def list_data_connectors_filepath(
        self,
        data_connector_name: str,
        bucket_name: Optional[str] = None,
        root_folder: Optional[str] = None,
    ) -> str | Dict:
        """List file paths within the specified data connector. For S3/GCS connectors you may need to provide a bucket_name; for SFTP connectors you may need to provide a root_folder.

        :param data_connector_name: name of the data connector
        :param bucket_name: Required for S3 & GCS
        :param root_folder: Root folder of SFTP
        """
        if not data_connector_name:
            return "Missing argument data_connector_name"
        if not self.organization_id:
            return "No Organization id found"

        def get_connector() -> str | pd.DataFrame:
            """Retrieve connector metadata for the given link service name.
            Reads from internal state or a backend client as needed."""
            url = build_list_data_connector_url(
                LIST_DATA_CONNECTORS, None, self.organization_id
            )
            res = self.api_client.post(url)

            if res["success"]:
                df = pd.DataFrame(res["details"])
                filtered_df = df.loc[df["link_service_name"] == data_connector_name]
                if filtered_df.empty:
                    return "No data connector found"
                return filtered_df

            return res["details"]

        connectors = get_connector()
        if isinstance(connectors, pd.DataFrame):
            value = connectors.loc[
                connectors["link_service_name"] == data_connector_name,
                "link_service_type",
            ].values[0]
            ds_type = value

            if ds_type == "s3" or ds_type == "gcs":
                if not bucket_name:
                    return "Missing argument bucket_name"

            if ds_type == "sftp":
                if not root_folder:
                    return "Missing argument root_folder"

        if self.organization_id:
            url = f"{LIST_FILEPATHS}?organization_id={self.organization_id}&link_service_name={data_connector_name}&bucket_name={bucket_name}&root_folder={root_folder}"
        res = self.api_client.get(url)

        if res.get("message", None):
            print(res["message"])
        return res["details"]

    def credits(self):
        """Return credit usage and quota information for the organization."""
        url = build_list_data_connector_url(
            COMPUTE_CREDIT_URI, None, self.organization_id
        )
        res = self.api_client.get(url)
        return res["details"]

    def update_user_access_for_organization(
        self,
        user_email: str,
        access_type: Literal["admin", "user"],
    ) -> str:
        """Change the role of a user within the organization. Accepts the user’s email and the new access type (admin or user) and returns a confirmation message.

        :param user_email: Email of user to be added to project.
        :param access_type: access type to be given to user (admin | user)
        :return: response
        """
        if access_type not in ["admin", "user"]:
            raise ValueError("access_type must be either 'admin' or 'user'")
        payload = {
            "organization_user_email": user_email,
            "organization_id": self.organization_id,
            "organization_admin": True if access_type == "admin" else False,
        }
        res = self.api_client.post(UPDATE_ORGANIZATION_URI, payload)

        if not res["success"]:
            raise Exception(res.get("details", "Failed to update user access"))

        return res.get("details", "User access updated successfully")
