import asyncio
from typing import Literal

import numpy as np
import pandas as pd
from authlib.integrations.httpx_client import AsyncOAuth2Client, OAuth2Client


class SciNoteClient(object):
    def __init__(
        self,
        client_id: str,
        client_secret: str,
        redirect_uri: str,
        server_url: str,
        token_saver: callable,
        code: str = None,
        refresh_token: str = None,
    ):
        self.client_id = client_id
        self.client_secret = client_secret
        self.redirect_uri = redirect_uri
        self.server_url = server_url
        self.token_saver = token_saver
        self.code = code
        self.refresh_token = refresh_token
        self.access_token = None
        self.token = None

        self.initialize()

    def initialize(self):
        if self.token is None:
            if self.refresh_token:
                self.refresh_access_token()
            elif self.code:
                self.fetch_access_token()
            else:
                raise Exception("No code or refresh token provided")

    def fetch_access_token(self):
        client = OAuth2Client(
            client_id=self.client_id,
            client_secret=self.client_secret,
            redirect_uri=self.redirect_uri,
        )
        token = client.fetch_token(
            url=self.server_url + "/oauth/token",
            grant_type="authorization_code",
            code=self.code,
        )
        self.update_token(token)

    def refresh_access_token(self):
        client = OAuth2Client(
            client_id=self.client_id,
            client_secret=self.client_secret,
            redirect_uri=self.redirect_uri,
        )
        token = client.refresh_token(
            url=self.server_url + "/oauth/token",
            refresh_token=self.refresh_token,
        )
        self.update_token(token)

    def update_token(self, token):
        self.access_token = token["access_token"]
        self.refresh_token = token["refresh_token"]
        self.token = token
        self.token_saver(token)

    def sync_request(self, method, url, json=None):
        with OAuth2Client(
            client_id=self.client_id,
            client_secret=self.client_secret,
            token=self.token,
            update_token=self.update_token,
        ) as client:
            return client.request(method, url, json=json)

    def get(self, url):
        return self.sync_request("GET", url)

    def post(self, url, data):
        return self.sync_request("POST", url, json=data)

    async def async_request(self, method, url):
        with AsyncOAuth2Client(
            client_id=self.client_id,
            client_secret=self.client_secret,
            token=self.token,
            update_token=self.update_token,
        ) as client:
            return client.request(method, url)

    async def async_get(self, urls):
        tasks = []
        for url in urls:
            tasks.append(self.async_request("GET", url))
        return await asyncio.gather(*tasks)

    def build_url_list(self, url):
        r = self.get(url)
        last_page = r.links.get("last", {}).get("page")
        if last_page:
            return [f"{url}?page={i}" for i in range(1, last_page + 1)]

    def get_teams(self, without_emoji=False):
        url = self.server_url + "/api/v1/teams"
        r = self.get(url)
        r_json = r.json()

        while r.links.get("next"):
            r = self.get(r.links.get("next").get("url"))
            r_json.extend(r.json())

        teams_id_dict = {
            team["attributes"]["name"].replace("🟢 ", "")
            if without_emoji
            else team["attributes"]["name"]: team["id"]
            for team in r_json["data"]
            if "🟢" in team["attributes"]["name"]
        }

        return teams_id_dict

    def get_inventories(self, team_id):
        url = self.server_url + f"/api/v1/teams/{team_id}/inventories"
        r = self.get(url)
        r_json = r.json()

        while r.links.get("next"):
            r = self.get(r.links.get("next").get("url"))
            r_json.extend(r.json())

        inventories_id_dict = {
            inventory["attributes"]["name"]: inventory["id"]
            for inventory in r_json["data"]
        }

        return inventories_id_dict

    def get_inventory_columns(self, inventory_id):
        url = self.server_url + f"/api/v1/inventories/{inventory_id}/columns"
        r = self.get(url)
        r_json = r.json()

        while r.links.get("next"):
            r = self.get(r.links.get("next").get("url"))
            r_json.extend(r.json())

        columns_id_dict = {}
        columns_types_dict = {}
        for column in r_json["data"]:
            columns_id_dict[column["id"]] = column["attributes"]["name"]
            columns_types_dict[column["id"]] = column["attributes"]["data_type"]

        return columns_id_dict, columns_types_dict

    def get_inventory_column_options(
        self,
        inventory_id,
        column_id,
        type: Literal["list", "status", "checklist", "stock_unit"],
    ):
        url = (
            self.server_url
            + f"/api/v1/inventories/{inventory_id}/columns/{column_id}/{type}_items"
        )
        r = self.get(url)
        r_json = r.json()

        while r.links.get("next"):
            r = self.get(r.links.get("next").get("url"))
            r_json.extend(r.json())

        options_id_dict = {
            option["attributes"]["name"]: option["id"] for option in r_json["data"]
        }

    def get_inventory_items(self, inventory_id):
        urls = self.build_url_list(
            self.server_url + f"/api/v1/inventories/{inventory_id}/inventory_items"
        )
        r = asyncio.run(self.async_get(urls))
        r_json = [r_.json() for r_ in r]

        data_df = pd.json_normalize(r_json[0]["data"])
        included_df = pd.json_normalize(r_json[0]["included"])

        for r_ in r_json[1:]:
            data_df = pd.concat(
                [data_df, pd.json_normalize(r_["data"])], ignore_index=True
            )
            included_df = pd.concat(
                [included_df, pd.json_normalize(r_["included"])], ignore_index=True
            )

        data_df = data_df[
            ["id", "attributes.name", "relationships.inventory_cells.data"]
        ]
        data_df = data_df.rename(columns={"attributes.name": "Name"})

        included_df = included_df.set_index("id")

        columns_dict, columns_types_dict = self.get_inventory_columns(inventory_id)
        stock_dict = {}

        for id, name in columns_dict.items():
            data_df[id] = np.nan

            if columns_types_dict[id] == "stock":
                stock_dict[id + " Units"] = "Stock Units"
                stock_dict[id + " Threshold"] = "Stock Threshold"

    def get_project_folders(self, team_id):
        url = self.server_url + f"/api/v1/teams/{team_id}/project_folders"
        r = self.get(url)
        r_json = r.json()

        while r.links.get("next"):
            r = self.get(r.links.get("next").get("url"))
            r_json.extend(r.json())

        folders_id_dict = {
            folder["attributes"]["name"]: folder["id"] for folder in r_json["data"]
        }

        return folders_id_dict

    def create_project_folder(self, team_id, parent_folder_id, name):
        url = self.server_url + f"/api/v1/teams/{team_id}/project_folders"
        data = {
            "data": {
                "type": "project_folders",
                "attributes": {"name": name, "parent_folder_id": parent_folder_id},
            }
        }
        r = self.post(url, data)

        return r.json()
