# AUTOGENERATED! DO NOT EDIT! File to edit: 01_projects.ipynb (unless otherwise specified).

__all__ = ['VersionManager', 'Version', 'ProjectManager', 'Project']

# Cell
import time
import urllib.parse
import webbrowser
import requests
from pathlib import Path
from slugify import slugify
from tightai import exceptions
from .exceptions import ArgumentsRequired
from .lookup import Lookup
from .base import BaseObject, base_tightignore
from .conf import CLI_ENDPOINT
from .downloaders import DownloadVersion
from .user import UserObject
from .upload import Uploader
from .utils import sort_by_key, print_dict

# Cell
class VersionManager(Lookup, BaseObject):
    api = CLI_ENDPOINT
    version = None
    project_id = None

    def __str__(self):
        return f"<VersionManager>"

    def __repr__(self):
        return self.__str__()

    def create(self, project_id=None, raw=False, **kwargs):
        '''
        Creates an empty project version.
        '''
        if project_id == None:
            raise ArgumentsRequired("project_id")
        r = self.http_post(f"/projects/{project_id}/versions/", data=kwargs)
        if raw:
            return r
        self.handle_invalid_lookup(r, expected_status_code=201)
        return Version(**r.json())

    def get(self, version=None, project_id=None, raw=False):
        if project_id == None or version == None:
            raise ArgumentsRequired("version", "project_id")
        r = self.http_get(f"/projects/{project_id}/versions/{version}")
        if raw:
            return r
        self.handle_invalid_lookup(r, expected_status_code=200)
        return Version(**r.json())

    def latest(self, project_id=None, none_on_404=False, deployed=False):
        if project_id == None:
            raise ArgumentsRequired("project_id")
        deployed_str = f"{deployed}".lower()
        r = self.http_get(f"/projects/{project_id}/latest/?deployed={deployed_str}")
        if deployed == True:
            none_on_404 = True
        if r.status_code == 404 and none_on_404:
            return None
        self.handle_invalid_lookup(r, expected_status_code=200)
        return Version(**r.json())

    def all(self, project_id=None, **kwargs):
        if project_id == None:
            raise ArgumentsRequired("project_id")
        endpoint = f"/projects/{project_id}/versions/"
        if len(kwargs.keys()):
            query_string = urllib.parse.urlencode(kwargs)
            endpoint = f"{endpoint}?{query_string}"
        r = self.http_get(endpoint)
        self.handle_invalid_lookup(r, expected_status_code=200)
        return [Version(**x) for x in r.json()['results']]

    def get_or_create(self, **kwargs):
        created = False
        version = None
        try:
            existing_r = self.get(**kwargs)
        except:
            existing_r = None
        if existing_r != None:
            return existing_r, created
        new_v = self.create(**kwargs)
        created = True
        return new_v, created

# Cell
class Version(Lookup, BaseObject):
    '''
    An individual release of any given project.
    '''
    version = None
    project_id = None
    trigger_options = ['status', 'build', 'build_and_deploy', 'deploy', 'destroy']
    display_options = ['project_id', 'version', 'description', 'url', 'deployed',
                       'deployed_timestamp', 'destroyed', 'destroyed_timestamp', 'updated', 'timestamp']

    def __str__(self):
        return f"<Version {self.version} ({self.project_id})>"

    objects = VersionManager()

    def build(self):
        return self.update_trigger(option='build')

    def delete(self):
        confirmation_code = f"{self.project_id}-v{self.version}"
        response = input(f"Deleting is cannot be reversed. Please confirm by typing:\n\n{confirmation_code}\n\n")
        if f"{response}".strip() != confirmation_code:
            print(f"\nDelete failed. {response} did not match {confirmation_code} exactly.")
            return
        r = self.http_delete(f"/projects/{self.project_id}/versions/{self.version}")
        self.handle_invalid_lookup(r, expected_status_code=204)
        setattr(self, 'deleted', True)
        return r

    def summary(self, verbose=0):
        data = {
            "version": self.version,
            "project_id": self.project_id,
            "url": self.url,
            "deployed": self.deployed,
            "updated": self.updated
        }
        if verbose == 1:
            print_dict(data)
        return data

    def deploy(self):
        raise Exception("Use `.push(local_path)` to deploy instead.")

    def destroy(self):
        return self.update_trigger(option='destroy')

    def upload(self, path=".", verbose=True):
        src_path = Path(path).resolve()
        tightai_ignore = src_path / ".tightignore"
        if not tightai_ignore.exists():
            print("No .tightignore found. Creating default.")
            with open(str(tightai_ignore), "w") as f:
                f.writelines(base_tightignore)
        uploader = Uploader(path=path, project_id=self.project_id, version=self.version)
        uploaded_count = uploader.upload(verbose=verbose)
        return uploaded_count

    def status(self, wait=True, display=True):
        endpoint = f"/projects/{self.project_id}/versions/{self.version}/status/"
        r = self.http_post(endpoint, data={})
        self.handle_invalid_lookup(r, expected_status_code=200)
        r_data = r.json()
        for k,v in r_data.items():
            setattr(self, k, v)
        verbose = 1
        if display == False:
            verbose = 0
        return self.details(verbose=verbose)

    def refresh(self):
        version = self.objects.get(version=self.version, project_id=self.project_id)
        for k,v in version.__dict__.items():
            setattr(self, k, v)
        return version.details()

    def push(self, path, check_status=True):
        project_id = self.project_id
        version = self.version
        uploaded_count = self.upload(path, verbose=True)
        if uploaded_count == 0:
            print(f"Uploaded 0 files. Continuing")
        print("Deploying...")
        deploy_r = self.update_trigger(option='build_and_deploy')
        if deploy_r.status_code != 200:
            raise Exception("Project not deployed. Please try again.")
        if not check_status:
            print(f"{project_id} {version} pushed. Use `.status()` to check deployment status.")
            return None
        details = self.status(wait=True)
        url = details.get('url')
        if not url:
            print("URL is not ready yet. Use `.status()` to check deployment status.")
            return None
        setattr(self, 'url', url)
        return url

    def predict(self, data={}, json={}, files=None, method='POST'):
        is_invalid = len(data.keys()) == 0 and files == None and len(json.keys()) == 0
        if is_invalid:
            raise Exception("You must include `files` or keyword arguments.")
        url = self.url
        if not url:
            raise Exception(f"Project {self.project_id} v{self.version} is not ready yet.")
        r = requests.request(method, url, data=data, json=json, files=files)
        if r.status_code not in range(200, 299):
            print(r.text)
            return
        return r.json()

    def download(self, path, overwrite=False):
        dest = Path(path).resolve()
        project_id = self.project_id
        version = self.version
        dl = DownloadVersion(path=path, project_id=project_id, version=version)
        dl.download(overwrite=overwrite)

    def update_trigger(self, option='status'):
        if option not in self.trigger_options:
            print(f"{option} is invalid. Please try again.")
            option = 'status'
        data = {
            'trigger_active': True,
            'trigger': f"{option}",
        }
        r = self.http_put(f"/projects/{self.project_id}/versions/{self.version}", data=data)
        self.handle_invalid_lookup(r, expected_status_code=200)
        self.update_from_response(r)
        return r

    def update_from_response(self, response, display=False):
        for k, v in response.json().items():
            if display:
                print(f"{k}")
            current_v = getattr(self, k)
            if v != None and current_v != v:
                setattr(self, k, v)
        return

    def open(self):
        details = self.details(verbose=0)
        url = details.get("url")
        if url == None or details['deployed'] == False:
            raise Exception("This version has not been deployed yet. Push a deployment or refresh version status.")
        return webbrowser.open(url)

# Cell
class ProjectManager(Lookup, BaseObject):
    api = CLI_ENDPOINT
    version = None

    def __str__(self):
        return f"<ProjectManager>"

    def __repr__(self):
        return self.__str__()

    def create(self, **kwargs):
        r = self.http_post(f"/projects/", data=kwargs)
        self.handle_invalid_lookup(r, expected_status_code=201)
        return Project(**r.json())

    def get(self, project_id=None):
        if project_id == None:
            raise ArgumentsRequired("project_id")
        r = self.http_get(f"/projects/{project_id}/")
        self.handle_invalid_lookup(r, expected_status_code=200)
        return Project(**r.json())

    def all(self, **kwargs):
        endpoint = f"/projects/"
        if len(kwargs.keys()):
            query_string = urllib.parse.urlencode(kwargs)
            endpoint = f"{endpoint}?{query_string}"
        r = self.http_get(endpoint)
        self.handle_invalid_lookup(r, expected_status_code=200)
        return [Project(**x) for x in r.json()['results']]

    def get_or_create(self, **kwargs):
        created = False
        if "project_id" in kwargs:
            existing_r = self.get(project_id=kwargs.get('project_id'))
            if existing_r.status_code == 200:
                return existing_r, created
        else:
            print("`project_id` was not included. Creating...")
        new_r = self.create(**kwargs)
        self.handle_invalid_lookup(new_r, expected_status_code=201)
        created = True
        return new_r, created


class Project(Lookup, BaseObject):
    project_id = None
    latest_version_obj = None
    display_options = ['name', 'id', 'description',
                       'updated', 'timestamp', 'versions_metadata', 'versions_urls']

    def __str__(self):
        return f"<Project {self.id}>"

    def summary(self, verbose=0):
        data = {
            "name": self.name,
            "project_id": self.project_id,
            "versions_summary": {
                "deployed": self.versions_metadata.get('deployed_count'),
                "urls": self.versions_urls,
            },
            "updated": self.updated
        }
        if verbose == 1:
            print_dict(data)
        return data

    @property
    def project_id(self):
        return self.id

    objects = ProjectManager()

    def create_version(self, version=None):
        if version == None:
            raise ArgumentsRequired("version")
        version, _ = Version.objects.get_or_create(project_id=self.project_id, version=version)
        return version

    def get_version(self, version=None):
        if version == None:
            raise ArgumentsRequired("version")
        version, _ = Version.objects.get_or_create(project_id=self.project_id, version=version)
        return version

    def delete(self):
        print("You can only delete projects in the tight.ai console right now.")

    def get_versions(self):
        versions = Version.objects.all(project_id=self.project_id)
        return versions

    def latest(self, deployed=False):
        if self.latest_version_obj == None:
            self.latest_version_obj = Version.objects.latest(project_id=self.project_id, deployed=deployed)
        return self.latest_version_obj

    def status(self, latest=True, version=None):
        if latest== False and version == None:
            raise Exception(f"`latest` or `version` must be set.")
        if latest and version == None:
            print("Get project status from latest version")
            version_obj = self.latest()
            return version_obj.status()
        version_obj = self.get_version(version=version)
        return version_obj.status()

    def open(self):
        version_obj = self.latest()
        version_obj.open()

    def push(self, path=None):
        if path == None:
            raise Exception("path is required.")
        return self.latest().push(path=path)

    def predict(self, data={}, json={}, files=None, method='POST', use_latest=True, version=None):
        if version == None and use_latest == False:
            raise Exception("You must include a version or pass `use_latest=True`")
        if version != None and use_latest == False:
            vers_obj = self.get_version(version=version)
        else:
            vers_obj = self.latest()
        return vers_obj.predict(data=data, json=json, files=files, method=method)