import os
import sys
from typing import Any, Dict, List, Optional, Tuple

from novem.exceptions import Novem403, Novem404

from ..api_ref import NovemAPI
from ..shared import NovemShare
from ..tags import NovemTags
from ..utils import cl
from ..utils import colors as clrs
from .files import NovemFiles


class NovemVisAPI(NovemAPI):
    shared: NovemShare
    tags: NovemTags
    files: Optional[NovemFiles] = None

    _vispath: Optional[str] = None
    _debug: bool = False

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)

        self.user = None

        if "debug" in kwargs and kwargs["debug"]:
            self._debug = True

        if "create" not in kwargs or kwargs["create"]:
            # let's create our plot if -C specified, always
            # create when used as an api unless specifically told not to
            self.api_create("")

        if "user" in kwargs and kwargs["user"]:
            self.user = kwargs["user"]

        if "qpr" in kwargs and kwargs["qpr"]:
            self._qpr = kwargs["qpr"].replace(",", "&")

        if self.user:
            base_path = f"users/{self.user}/vis/{self._vispath}/{self.id}"
        else:
            base_path = f"vis/{self._vispath}/{self.id}"
        self.shared = NovemShare(self, base_path)
        self.tags = NovemTags(self, base_path)
        self.files = NovemFiles(self)

    def __setattr__(self, name: str, value: Any) -> None:
        if name == "shared" and hasattr(self, "shared") and not isinstance(value, NovemShare):
            self.shared.set(value)
        elif name == "tags" and hasattr(self, "tags") and not isinstance(value, NovemTags):
            self.tags.set(value)
        else:
            super().__setattr__(name, value)

    def api_dump(self, outpath: str) -> None:
        """
        Iterate over current id and dump output to supplied path
        """

        qpath = f"{self._api_root}vis/{self._vispath}/{self.id}/"

        if self.user:
            qpath = f"{self._api_root}users/{self.user}/vis/" f"{self._vispath}/{self.id}/"

        # create util function
        def rec_tree(path: str) -> None:
            qp = f"{qpath}{path}"
            fp = f"{outpath}{path}"
            # print(f"QP: {qp}")
            req = self._session.get(qp)

            if not req.ok:
                return None

            headers = req.headers
            tp = headers.get("X-NVM-Type", headers.get("X-NS-Type", "file"))

            # if i am a file, write to disc
            if tp == "file":
                # skip files with default values
                if headers.get("x-nvm-default", "").lower() == "true":
                    print(f"Skipping default: {fp}")
                    return None
                # ensure parent directory exists before writing
                parent_dir = os.path.dirname(fp)
                if parent_dir and not os.path.exists(parent_dir):
                    os.makedirs(parent_dir, exist_ok=True)
                    print(f"Creating folder: {parent_dir}")
                with open(fp, "w") as f:
                    f.write(req.text)
                print(f"Writing file:    {fp}")
                return None

            # if I am a folder, recurse without creating yet
            nodes: List[Dict[str, str]] = req.json()

            # Recurse relevant structure (skip system entries and read-only files)
            for r in nodes:
                if r["type"] in ["system_file", "system_dir"]:
                    continue
                child_path = f'{path}/{r["name"]}'
                child_fp = f"{outpath}{child_path}"

                # /shared/ and /tags/ are special markers - create empty files from listing
                if r["type"] in ["file", "link"] and (
                    child_path.startswith("/shared/") or child_path.startswith("/tags/")
                ):
                    parent_dir = os.path.dirname(child_fp)
                    if parent_dir and not os.path.exists(parent_dir):
                        os.makedirs(parent_dir, exist_ok=True)
                        print(f"Creating folder: {parent_dir}")
                    with open(child_fp, "w") as f:
                        f.write("")
                    print(f"Writing file:    {child_fp}")
                    continue

                # skip read-only files/links
                if r["type"] in ["file", "link"] and "w" not in r.get("permissions", []):
                    continue
                rec_tree(child_path)

        # start recurison
        rec_tree("")

    def api_load(self, inpath: str) -> None:
        """
        Load a dumped folder structure back into the API.
        Walks the folder and for each file: PUT to create, then POST content.
        """

        qpath = f"{self._api_root}vis/{self._vispath}/{self.id}"

        if self.user:
            print(f"You cannot modify another user's {self._vispath}")
            return

        def load_tree(local_path: str, api_path: str) -> None:
            full_local = os.path.join(inpath, local_path.lstrip("/")) if local_path else inpath

            if os.path.isfile(full_local):
                # Read file content
                with open(full_local, "r") as f:
                    content = f.read()

                full_api = f"{qpath}{api_path}"

                # Try PUT first to create the resource
                r = self._session.put(full_api)
                put_status = r.status_code

                # POST the content
                r = self._session.post(
                    full_api,
                    headers={"Content-type": "text/plain"},
                    data=content.encode("utf-8"),
                )
                print(f"Loaded file:     {api_path} (PUT: {put_status}, POST: {r.status_code}, {len(content)} bytes)")

            elif os.path.isdir(full_local):
                print(f"Processing dir:  {api_path or '/'}")

                # Iterate over directory contents
                for entry in sorted(os.listdir(full_local)):
                    entry_local = os.path.join(local_path, entry) if local_path else entry
                    entry_api = f"{api_path}/{entry}"
                    load_tree(entry_local, entry_api)

        # Start loading from root
        load_tree("", "")

    def api_tree(self, colors: bool = False, relpath: str = "/") -> str:
        """
        Iterate over the current id and print a "pretty" ascii tree
        """
        if relpath[0] != "/":
            relpath = f"/{relpath}"

        clrs()

        qpath = f"{self._api_root}vis/{self._vispath}/{self.id}{relpath}"

        if self.user:
            qpath = f"{self._api_root}users/{self.user}/vis/" f"{self._vispath}/{self.id}{relpath}"

        # TODO: we're using some hard coded unicode symbols and colors here
        # probably better to make this configurable by the user and perhaps
        # even move it to cli

        # this is an ugly hack, and mostly here to help users familiarize
        # themselves with the api structure

        # some display options
        c = "├"
        b = "└"
        v = "│"
        h = "─"

        # create util function
        def rec_tree(path: str, level: int = 0, last: List[bool] = [False]) -> Tuple[List[str], str]:
            qp = f"{qpath}{path}"
            req = self._session.get(qp)

            if not req.ok:
                return ([], "")

            headers = req.headers
            tp = headers.get("X-NVM-Type", headers.get("X-NS-Type", "file"))

            if tp == "file":
                print("The tree display is only available for `dir` paths")
                sys.exit(-1)

            nodes: List[Dict[str, str]] = req.json()

            hdp = []
            if level == 0:
                hdp = headers.get("X-NVM-Permissions", headers.get("X-NS-Permissions", "")).split(", ")

            pfx = ""
            for il in last:
                if il:
                    pfx += "    "
                else:
                    pfx += f"{v}   "

            # drop system stuff
            nodes = [x for x in nodes if x["type"] not in ["system_file", "system_dir"]]

            resp = ""
            # convert element into a tree structure
            nodes = sorted(nodes, key=lambda k: (k["type"], k["name"]))
            for r in nodes:
                rd = "r" if "r" in r["permissions"] else "-"
                w = "w" if "w" in r["permissions"] else "-"
                d = "d" if "d" in r["permissions"] else "-"
                if colors:
                    a = f"{cl.FGGRAY}[{rd}{w}{d}]{cl.ENDC}"
                else:
                    a = f"[{rd}{w}{d}]"

                if r["name"] == nodes[-1]["name"]:
                    mc = last + [True]
                    co = f"{b}"
                else:
                    mc = last + [False]
                    co = f"{c}"

                if r["type"] == "dir":
                    if colors:
                        resp += f"{pfx}{co}{h}{h} {a} {cl.OKBLUE}" f'{r["name"]}/{cl.ENDC}\n'
                    else:
                        resp += f'{pfx}{co}{h}{h} {a} {r["name"]}/\n'

                    resp += rec_tree(f'{path}/{r["name"]}', level + 1, mc)[1]
                else:
                    resp += f'{pfx}{co}{h}{h} {a} {r["name"]}\n'

            # order by dir, files, alphabetically
            return (hdp, resp)

        hdp, tr = rec_tree("/", 0, [True])

        sf = f"{self.id}{relpath}"
        if sf[-1] != "/":
            sf = f"{sf}/"

        if colors:
            sf = f"{cl.OKBLUE}{sf}{cl.ENDC}"

        rd = "r" if "r" in hdp else "-"
        w = "w" if "w" in hdp else "-"
        d = "d" if "d" in hdp else "-"
        if colors:
            a = f"{cl.FGGRAY}[{rd}{w}{d}]{cl.ENDC}"
        else:
            a = f"[{rd}{w}{d}]"
        tr = f"{a} {sf}\n{tr}"

        return tr[:-1]  # strip trailing newline

    def api_read(self, relpath: str) -> str:
        """
        Read the api value located at realtive path
        """

        qpath = f"{self._api_root}vis/{self._vispath}/{self.id}{relpath}"

        # We can read information from other users, but not perform any
        # other actions so only the GET method supports the custom user
        # pathing
        if self.user:
            qpath = f"{self._api_root}users/{self.user}/vis/" f"{self._vispath}/{self.id}{relpath}"

        if self._qpr and len(self._qpr):
            qpath = f"{qpath}?{self._qpr}"

        if self._debug:
            print(f"GET: {qpath}")

        r = self._session.get(qpath)

        # TODO: verify result and raise exception if not ok
        if r.status_code == 404:
            raise Novem404(qpath)

        if r.status_code == 403:
            raise Novem403

        return r.content.decode("utf-8")

    def api_read_bytes(self, relpath: str) -> bytes:
        qpath = f"{self._api_root}vis/{self._vispath}/{self.id}{relpath}"

        # We can read information from other users, but not perform any
        # other actions so only the GET method supports the custom user
        # pathing
        if self.user:
            qpath = f"{self._api_root}users/{self.user}/vis/" f"{self._vispath}/{self.id}{relpath}"

        if self._qpr and len(self._qpr):
            qpath = f"{qpath}?{self._qpr}"

        if self._debug:
            print(f"GET: {qpath}")

        r = self._session.get(qpath)

        # TODO: verify result and raise exception if not ok
        if r.status_code == 404:
            raise Novem404(qpath)

        if r.status_code == 403:
            raise Novem403

        return r.content

    def api_delete(self, relpath: str) -> None:
        """
        relpath: relative path to the plot baseline /config/type
                 for the type file in the config folder
        value: the value to write to the file
        """
        if self.user:
            print(f"You cannot modify another user's {self._vispath}")
            return

        path = f"{self._api_root}vis/{self._vispath}/{self.id}{relpath}"

        if self._debug:
            print(f"DELETE: {path}")

        r = self._session.delete(path)

        if r.status_code == 404:
            raise Novem404(path)

        if r.status_code == 403:
            raise Novem403

        # TODO: verify result and raise exception if not ok
        if not r.ok:
            print(r)
            print(f"DELETE: {path}")
            print("body")
            print("---")
            print(r.text)
            print("headers")
            print("---")
            print(r.headers)
            print("should raise an error")

    def api_create(self, relpath: str) -> None:
        """
        relpath: relative path to the plot baseline /config/type
                 for the type file in the config folder
        value: the value to write to the file
        """
        if self.user:
            print(f"You cannot modify another user's {self._vispath}")
            return

        path = f"{self._api_root}vis/{self._vispath}/{self.id}{relpath}"

        if self._debug:
            print(f"PUT: {path}")

        r = self._session.put(path)

        if r.status_code == 404:
            raise Novem404(path)

        if r.status_code == 403:
            raise Novem403(path)

        if r.status_code == 409:
            # we will ignore 409 errors
            # as creating objects that already exist is not a problem
            return

        # TODO: verify result and raise exception if not ok
        if not r.ok:
            print(r)
            print(f"PUT: {path}")
            print("body")
            print("---")
            print(r.text)
            print("headers")
            print("---")
            print(r.headers)
            print("should raise a general error")

    def api_write(self, relpath: str, value: str) -> None:
        """
        relpath: relative path to the plot baseline /config/type
                 for the type file in the config folder
        value: the value to write to the file
        """
        if self.user:
            print(f"You cannot modify another user's {self._vispath}")
            return

        path = f"{self._api_root}vis/{self._vispath}/{self.id}{relpath}"

        if self._debug:
            print(f"POST: {path}")

        r = self._session.post(
            path,
            headers={"Content-type": "text/plain"},
            data=value.encode("utf-8"),
        )

        if r.status_code == 404:
            raise Novem404(path)

        if r.status_code == 403:
            raise Novem403

        # TODO: verify result and raise exception if not ok
        if not r.ok:
            print(r)
            print(f"POST: {path} {value}")
            print("body")
            print("---")
            print(r.text)
            print(r.status_code)
            print("headers")
            print("---")
            for k, v in r.headers.items():
                print(f"   {k}: {v}")
            print("should raise a general error")

    @property
    def log(self) -> None:
        """
        print the current novem logs for the given vis
        """
        print(self.api_read("/log"))

        return None

    @property
    def qpr(self) -> str:
        if self._qpr:
            return self._qpr.replace("&", ",")
        else:
            return ""

    @qpr.setter
    def qpr(self, value: str) -> None:
        if value:
            self._qpr = value.replace(",", "&")
