import os
import yaml
import traceback
import requests
from usdm_excel.errors_and_logging.errors_and_logging import ErrorsAndLogging


class CDISCCTLibrary:
    API_ROOT = "https://api.library.cdisc.org/api"
    HEADERS = {
        "Content-Type": "application/json",
        "api-key": os.getenv("CDISC_API_KEY"),
    }

    def __init__(self, errors_and_logging: ErrorsAndLogging):
        self._errors_and_logging = errors_and_logging
        f = open(os.path.join(os.path.dirname(__file__), "data", "missing_ct.yaml"))
        self._missing_ct = yaml.load(f, Loader=yaml.FullLoader)
        f = open(
            os.path.join(os.path.dirname(__file__), "data", "cdisc_ct_config.yaml")
        )
        self._cdisc_ct_config = yaml.load(f, Loader=yaml.FullLoader)
        # Visible properties
        self.version = self._cdisc_ct_config["version"]
        self.system = "http://www.cdisc.org"
        # Private properties
        self._by_code_list = {}
        self._by_term = {}
        self._by_submission = {}
        self._by_pt = {}
        self._by_klass_attribute = {}
        if self._code_lists_exist():
            self._load_ct()
        else:
            self._get_ct()
            self._save_code_lists(self._by_code_list)
        self._get_missing_ct()
        self._get_klass_attribute()

    def submission(self, value, cl=None):
        if value in list(self._by_submission.keys()):
            concept_ids = self._by_submission[value]
            if len(concept_ids) == 0:
                return None
            elif len(concept_ids) == 1:
                code_list = self._by_code_list[concept_ids[0]]
                return next(
                    (
                        item
                        for item in code_list["terms"]
                        if item["submissionValue"] == value
                    ),
                    None,
                )
            else:
                if cl and cl in concept_ids:
                    code_list = self._by_code_list[cl]
                    return next(
                        (
                            item
                            for item in code_list["terms"]
                            if item["submissionValue"] == value
                        ),
                        None,
                    )
                else:
                    return None
        else:
            return None

    def preferred_term(self, value, cl=None):
        if value in list(self._by_pt.keys()):
            concept_ids = self._by_pt[value]
            if len(concept_ids) == 0:
                return None
            elif len(concept_ids) == 1:
                code_list = self._by_code_list[concept_ids[0]]
                return next(
                    (
                        item
                        for item in code_list["terms"]
                        if item["preferredTerm"] == value
                    ),
                    None,
                )
            else:
                if cl and cl in concept_ids:
                    code_list = self._by_code_list[cl]
                    return next(
                        (
                            item
                            for item in code_list["terms"]
                            if item["preferredTerm"] == value
                        ),
                        None,
                    )
                else:
                    return None
        else:
            return None

    def klass_and_attribute(self, klass, attribute, value):
        try:
            # if value == "Protocol Treatment Arm":
            #  print(f"Klass: {klass}, attribute: {attribute}, value: {value}")
            concept_id = self._by_klass_attribute[klass][attribute]
            # if value == "Protocol Treatment Arm":
            #  print(f"Concept: {concept_id}")
            code_list = self._by_code_list[concept_id]
            # if value == "Protocol Treatment Arm":
            #  print(f"Concept: {code_list}")
            return self._get_item(code_list, value)
        except Exception as e:
            self._errors_and_logging.exception(
                f"Failed to find '{value}' for klass '{klass}' attribute '{attribute}'",
                e,
            )
            return None

    def unit(self, value):
        try:
            code_list = self._by_code_list["C71620"]
            return self._get_item(code_list, value)
        except Exception as e:
            self._errors_and_logging.exception(f"Failed to find unit '{value}'")
            return None

    def _get_item(self, code_list, value):
        try:
            for field in ["conceptId", "preferredTerm", "submissionValue"]:
                # if value == "Protocol Treatment Arm":
                #  print(f"GET: {field}")
                result = next(
                    (
                        item
                        for item in code_list["terms"]
                        if item[field].upper() == value.upper()
                    ),
                    None,
                )
                if result:
                    return result
            return None
        except Exception as e:
            self._errors_and_logging.exception(
                f"Failed to find CDSIC CT for '{value}' in code ist '{code_list}'", e
            )
            return None

    def _get_ct(self):
        for item in self._cdisc_ct_config["required"]:
            self._get_code_list(item)

    def _load_ct(self):
        self._by_code_list = self._read_code_lists()
        for c_code, entry in self._by_code_list.items():
            for item in entry["terms"]:
                self._check_in_and_add(self._by_term, item["conceptId"], c_code)
                self._check_in_and_add(
                    self._by_submission, item["submissionValue"], c_code
                )
                self._check_in_and_add(self._by_pt, item["preferredTerm"], c_code)

    def _get_missing_ct(self):
        for response in self._missing_ct:
            self._by_code_list[response["conceptId"]] = response
            for item in response["terms"]:
                self._check_in_and_add(
                    self._by_term, item["conceptId"], response["conceptId"]
                )
                self._check_in_and_add(
                    self._by_submission, item["submissionValue"], response["conceptId"]
                )
                self._check_in_and_add(
                    self._by_pt, item["preferredTerm"], response["conceptId"]
                )

    def _get_klass_attribute(self):
        for klass, info in self._cdisc_ct_config["klass"].items():
            if not klass in self._by_klass_attribute:
                self._by_klass_attribute[klass] = {}
            for attribute, cl in info.items():
                if not attribute in self._by_klass_attribute[klass]:
                    self._by_klass_attribute[klass][attribute] = cl

    def _get_code_list(self, c_code):
        for package in self._cdisc_ct_config["packages"]:
            package_full_name = "%sct-%s" % (package, self.version)
            api_url = self._url(
                "/mdr/ct/packages/%s/codelists/%s" % (package_full_name, c_code)
            )
            self._errors_and_logging.info(f"CDISC CT Library: {api_url}")
            raw = requests.get(api_url, headers=self.__class__.HEADERS)
            if raw.status_code == 200:
                response = raw.json()
                response.pop("_links", None)
                self._by_code_list[response["conceptId"]] = response
                for item in response["terms"]:
                    self._check_in_and_add(
                        self._by_term, item["conceptId"], response["conceptId"]
                    )
                    self._check_in_and_add(
                        self._by_submission,
                        item["submissionValue"],
                        response["conceptId"],
                    )
                    self._check_in_and_add(
                        self._by_pt, item["preferredTerm"], response["conceptId"]
                    )
                return
        # If none found in all of the packages, then log error.
        self._errors_and_logging.exception(
            f"Failed to find CDSIC CT for C code '{c_code}'"
        )

    def _url(self, relative_url):
        return f"{self.__class__.API_ROOT}{relative_url}"

    def _check_in_and_add(self, collection, id, item):
        if not id in collection:
            collection[id] = []
        collection[id].append(item)

    def _save_code_lists(self, data):
        try:
            if not self._code_lists_exist():
                with open(self._ct_filename(), "w") as f:
                    yaml.dump(data, f, indent=2, sort_keys=True)
        except Exception as e:
            self._errors_and_logging.exception("Failed to save CDSIC CT file", e)

    def _read_code_lists(self):
        try:
            if self._code_lists_exist():
                with open(self._ct_filename()) as f:
                    return yaml.safe_load(f)
            else:
                self._errors_and_logging.error(
                    f"Failed to read CDSIC CT file, does not exist"
                )
                return None
        except Exception as e:
            self._errors_and_logging.exception("Failed to read CDSIC CT file", e)

    def _code_lists_exist(self):
        return os.path.isfile(self._ct_filename())

    def _ct_filename(self):
        return os.path.join(
            os.path.dirname(__file__),
            "data",
            f"cdisc_ct_{self._cdisc_ct_config['version']}.yaml",
        )
