import json
import traceback

from libs.osaf.osaf_core import OSAF_Core
from libs.spider_logger.logger import SpiderLog
from libs.unirepo import search
from urllib.parse import urlparse
from typing import List, Dict, Optional, Tuple

from libs.unirepo.UniRepo2 import UPackage


class URI:
    proto = None
    path = None
    def __init__(self, uri_str : str):
        parsed = urlparse(uri_str)
        self.proto = parsed.scheme
        self.path = parsed.path

    def isLocalResource(self):
        return self.proto == 'file'


class UPackageBundle:


    def __init__(self, parent_package : UPackage, id):
        self.id = id
        self.tags = []
        self.files = []
        self.parent_package: UPackage
        self.full_url = None

        self.display_name = None
        self.description = None
        self.tags = None
        self.deps = []
        self.files = []
        self.ext = None
        self.note = None


        self.parent_package = parent_package
        self.full_url = f'{parent_package.full_url}/bundles/{id}/'
        self.parent_package.repo.unirepo.log.d(f'created object (parent UPackage:{parent_package.id}) UPackageVersion {id}')


    def load(self):
        l = self.full_url + "/bundle.json"
        b_raw = self.parent_package.repo.unirepo.getFileStr(l)
        b = json.loads(b_raw)


        self.display_name = b['display_name']
        self.description = b['description']
        self.tags = b['tags']
        self.deps = b['deps']
        f_list = b['files']
        for f in f_list:
            bf = BundleFile(f['filepath'],f['hash'],f['size'])
            self.files.append(bf)

        if b.get('note') is not None:
            self.note = b['note']
        if b.get('ext') is not None:
            self.ext = b['ext']
        if b.get('contained_name') is not None:
            self.contained_name = b['contained_name']
        else:
            self.contained_name = f'{self.parent_package.id}-{self.id}'


class BundleFile:
    def __init__(self, filename, hash=None, size=-1):
        self.filename=filename
        self.hash = hash
        self.size = size



class UniRepo:
    def __init__(self, logger : SpiderLog, osa : OSAF_Core, conf, sources):
        self.query_short_cuts = {
            "v": "version",
            "a": "arch",
            "c": "cert",
            "n": "name",
            "s": "source"
        }

        self.arch_aliases = conf.get("arch_aliases")

        print(self.arch_aliases)

        self.managed_sources = []

        self.data_base = []
        self.log = logger
        self.osa = osa
        self.conf = conf
        self.raw_sources = sources
        self.extra_filters = {}



        self.cache_made = False
        try:
            self.load_sources(sources)
            self.log.d("ok. sources loaded without critcal errors")
        except:
            self.log.e(traceback.format_exc())
            self.log.e(f'error load sources from {sources}')

    def simplequery2list(self, query_string: str):
        def warn(msg):
            self.log.w("warn >> " + msg)
        filters = {}
        a = query_string.split("?")

        filters['__unclean'] = []

        if len(a) == 1:
            filters['name'] = a[0]

        if len(a) == 2:
            filters['name'] = a[0]

            tags = a[1].split("+")

            for t in tags:
                x = t.split("=")

                if len(x) == 1:
                    if x[0] != '':
                        k = x[0]
                        if self.query_short_cuts[k] is not None: k = self.query_short_cuts[k]
                        filters[k] = '__no_value'
                    else:
                        warn("emtry quere part")

                if len(x) == 2:
                    if x[0] != '':
                        k = x[0]
                        if self.query_short_cuts.get(k) is not None: k = self.query_short_cuts[k]
                        v = x[1]
                        filters[k] = v
                    else:
                        warn("Uncorrect quere part:\"" + str(t) + "\"")
                        filters['__unclean'].append(t)

                if len(x) > 2:
                    filters['__unclean'].append(t)
                    warn("Uncorrect quere part:" + str(t))

        if len(a) > 2:
            warn(f'uncorrect query: "{query_string}"')
            filters['__unclean'].append(query_string)


        return filters


    def unpack_query(self, query):
        return self.simplequery2list(query)

    def pack_query(self, query):
        s = query.get('name')
        if s is None:
            s = "?"
        else:
            s = s + "?"

        for k, v in query.items():
            s = s + f'{k}={v}+'

        return s[:-1]




    def load_sources(self, sources):
        for s in sources['sources']:
            uri = URI(s.get('url'))
            enabled = s.get('enabled')
            self.log.d(f'source :{s}, isEnabled:{enabled}, uri:{uri.__dict__}')
            if enabled:
                self.log.d(f'ADD to managed_sources {uri.original_uri}')
                self.managed_sources.append(uri)
            else:
                self.log.w(f'Skipping source {uri.original_uri}')



    def getFileStr(self, uri):
        u = URI(uri)
        r = ""
        try:
            if u.proto == "http" or u.proto == "https":

                code, content = self.osa.web.getText(u.original_uri)
                if code == 200:
                    r = content
                else:
                    print("C" + str(code))

            if u.proto == "file":
                sf = u.path
                if self.osa.files.exists(sf):
                    r = self.osa.files.read_str(sf)
                else:
                    self.log.e(f'Source {sf} not found!')
        except:
            self.log.e(f"Fail to get {uri}")

        return r

    def make_cache(self):
        if self.cache_made:
            return




        self.log.i("making UniRepo cache...")
        data_base = []
        self.log.d(f'Loading data from {len(self.managed_sources)} managed_sources')
        for s in self.managed_sources:

            u : URI = s
            self.log.i(f'processing source {u.original_uri} ...')
            try:
                source = Source(self, u)
                source.read_repos()
                data_base.append(source)
            except:
                self.log.e(f"Fail to process source! {u.original_uri}")

        self.data_base = data_base
        self.cache_made = True


    def search(self, pkg_query: str, strict_names: bool = True) -> Tuple[List[UPackageBundle], List[Tuple]]:
        filters = self.simplequery2list(pkg_query)

        non_strict_results = []

        #fixme
        #move this shit to libsyspack code

        filters.update(self.extra_filters)


        search_results = []

        for source in self.data_base:
            for repo in source.repos:
                for package in repo.packages:
                    for pkg_version in package.versions:
                        v : UPackageBundle = pkg_version
                        # print(f'{v.parent_package.id} :: {v.id} ::  {v.tags}')

                        if (self.search_match(filters, v)):
                            search_results.append(v)

                        non_strict_results.append((v, search.levenstein(v.parent_package.id, filters['name'])))

        self.log.d(f'search results {pkg_query}: {len(search_results)}')
        for s in search_results:
             self.log.d(f'SR: {s.parent_package.id} :: {s.id}')

        return search_results, non_strict_results

    def search_match(self, filters, u_pkg_ver):

        def match(round, pkg_v, v):
            if k[:2] == "__": return round
            return round and pkg_v is not None and (str(pkg_v).lower() == str(v).lower() or str(v).lower() == 'any' or str(pkg_v).lower() == 'any')

        pkg_tagged = {}
        pkg_tagged['name'] = u_pkg_ver.parent_package.id
        pkg_tagged['bundle-id'] = u_pkg_ver.id
        pkg_tagged['source'] = u_pkg_ver.parent_package.repo.source.id
        pkg_tagged['repo'] = u_pkg_ver.parent_package.repo.id
        pkg_tagged = {**pkg_tagged, **u_pkg_ver.tags}

        self.log.d(f'SEARCH: {filters}')

        NextRound = True
        for k,v in filters.items():
            self.log.dd(f'filter k == {k} and v == {v}')
            self.log.dd(f'pkg k == {k} and v == {pkg_tagged.get(k)}')

            if isinstance(pkg_tagged.get(k), list):
                ks = False
                for x in pkg_tagged.get(k):
                    ks = ks or match(NextRound, x, v)
                NextRound = NextRound and ks

            else:
                NextRound = match(NextRound, pkg_tagged.get(k), v)
            self.log.dd(f'round {NextRound}')

        return NextRound

    def get_bundle(self, bundle_object, run_dir):

        uri = URI(bundle_object.full_url)

        self.log.d(f'getting bundle :{bundle_object.full_url}')
        file_srcs = []

        for f in bundle_object.files:
            self.log.dd(f.filename)
            if uri.proto == 'file':

                self.osa.files.cp(f'{uri.path}/{f.filename}', f'{run_dir}/{f.filename}')
            else:
                self.osa.web.getFileInDir(f'{uri.original_uri}/{f.filename}', run_dir, f.filename)


    def isLocalResource(self, path):
        return True

    def pkg_selector(self, bundle_list):
        if len(bundle_list) == 0: return None
        return bundle_list[0]

    def auto_get(self, query):
        sr, _ = self.search(query)
        return self.pkg_selector(sr)

    def export(self, bundle_data, output_dir):
        return self.get_bundle(bundle_data, output_dir)

class Source:
    def __init__(self, ur: UniRepo, uri: URI):
        self.id = None
        self.repos_ids = []
        self.repos = []
        self.full_url = None

        self.full_url = uri.original_uri

        if uri.proto == "http" or uri.proto == "https":
            self.type = 'remote-http'
        if uri.proto == "file":
            self.type = 'local'


        self.uri = uri
        self.unirepo = ur

        content = self.unirepo.getFileStr(uri.original_uri + "/source.json")

        self.unirepo.log.dd(content)
        d = json.loads(content)

        self.id = d.get('id')
        if id is None:
            raise ValueError("Source id is Null in recived data!")

        for r in d['repos']:
            if r['enabled']:
                self.repos_ids.append(r['id'])
            else:
                self.unirepo.log.d(f'skipping disabled repo {r["id"]} from source {id}')



    def read_repos(self):
        for r_id in self.repos_ids:
            self.unirepo.log.d(f'ADD: {r_id} {len(self.repos_ids)}')
            self.repos.append(Repo(self, r_id))



class Repo:
    def __init__(self, source, id):

        self.id = id
        self.source = source
        self.unirepo = source.unirepo
        self.full_url = f'{source.full_url}/{id}/'
        self.packages = []

        repo_inf = self.unirepo.getFileStr(f'{self.source.uri.original_uri}/{id}/repo.json')
        repo_index = self.unirepo.getFileStr(f'{self.source.uri.original_uri}/{id}/index.json')
        repo_index = json.loads(repo_index)
        p_list = repo_index.get('packages')

        for p in p_list:
            try:
                pkg = UPackage(self, p)
                pkg.load()
                self.packages.append(pkg)
            except:
                self.unirepo.log.e(traceback.format_exc())
                self.unirepo.log.e(f'failed to load pkg {p}')



class UPackage:

    def __init__(self, repo, id):
        self.id = None
        self.versions = []
        self.full_url = None
        self.repo: Repo

        self.id = id
        self.repo = repo
        self.full_url = f'{repo.full_url}/{id}/'

    def load(self):
        pd = json.loads(self.repo.unirepo.getFileStr(self.full_url + "/package.json"))
        vers = pd['bundles']

        for v in vers:
            upv = UPackageBundle(self, v)
            upv.load()
            self.versions.append(upv)


