import configparser
from collections import OrderedDict
from datetime import datetime, timedelta
from multiprocessing.pool import Pool
import multiprocessing

from operator import itemgetter

import dateparser
import typer
from dateparser.search import search_dates
from safetypy import safetypy
from iart.modules import csvExporter
from tqdm import tqdm

from pprint import pprint as pp

IGNORED_TYPES = [
    "smartfield",
    "primeelement",
    "section",
    "category",
    "element",
    "information",
    "information - media",
    "information - ",
]
COMMON_DATA = [
    "AuditID",
    "Name",
    "DateStarted",
    "DateCompleted",
    "ConductedBy",
    "Percentage Score",
    "Score",
    "Site",
    "Area",
    "Region",
    "Template Name",
]
DELIMITER = chr(255)
FAILED_DELIMITER = " [F]"


class TemplateCreator:
    def __init__(
        self,
        template_id,
        sp,
        date_modified,
        date_before,
        header_only=False,
        completed="both",
    ):
        self.sp = sp
        self.header_only = header_only
        self.template_id = template_id
        self.most_recent_inspection = self.get_most_recent_inspection(completed)
        self.inspection_json = sp.get_audit(self.most_recent_inspection)
        self.current_table = csvExporter.CsvExporter(
            self.inspection_json, self.header_only
        )
        inspection_as_list = self.current_table.convert_audit_to_table()
        self.inspection_as_list = [
            item for item in inspection_as_list if item[1] not in IGNORED_TYPES
        ]
        self.prime_elements = self.map_prime_elements()
        self.inspection_list = sp.discover_audits(
            template_id=[template_id],
            modified_after=date_modified,
            modified_before=date_before,
            completed=completed,
        )
        self.all_inspections, self.full_combined = self.combine_inspections()
        self.csv_as_list = [
            {"AuditID": []},
            {"Name": []},
            {"DateStarted": []},
            {"DateCompleted": []},
            {"Percentage Score": []},
            {"Score": []},
            {"Site": []},
            {"Area": []},
            {"Region": []},
            {"Template Name": []},
        ]
        self.item_label_map, self.dict_label_map = self.map_item_id_to_label()
        manager = multiprocessing.Manager()
        self.L = manager.list()

    @staticmethod
    def chunks(lst, n):
        """Yield successive n-sized chunks from lst."""
        length = 0
        for i in range(0, len(lst), n):
            length += 1
            yield lst[i : i + n]

    def raise_pool(self, list_of_audits):
        # Establish the Pool
        self.L[:] = []
        threads = 4
        pool = Pool(processes=threads)
        list(
            tqdm(
                pool.imap(self.process_audit, list_of_audits), total=len(list_of_audits)
            )
        )
        pool.close()
        pool.join()
        return list(self.L)

    def process_audit(self, audit_id):
        # Appends downloaded audits to a shared list (L)
        audit_id = audit_id["audit_id"]
        downloaded_audit = self.sp.get_audit(audit_id)
        self.L.append(downloaded_audit)

    def get_most_recent_inspection(self, completed):
        list_of_inspections = self.sp.discover_audits(
            template_id=[self.template_id], limit=1, order="desc", completed=completed
        )
        if list_of_inspections is not None:
            if list_of_inspections["count"] > 0:
                return list_of_inspections["audits"][0]["audit_id"]
        else:
            return None

    def combine_inspections(self):
        all_inspections = []
        full_inspections = []
        if self.inspection_list:
            chunks_to_process = self.sp.chunks(self.inspection_list["audits"], 500000)
            number_of_inspections = len(self.inspection_list["audits"])
            print(f"Downloading {number_of_inspections} inspections")
            for chunk in chunks_to_process:
                audits_to_process = self.sp.raise_pool(chunk)
                inspection_pbar = tqdm(audits_to_process)
                for inspection in inspection_pbar:
                    audit_id = inspection["audit_id"]
                    inspection_pbar.set_description(f"Processing {audit_id}")
                    inspection_json = inspection
                    current_table = csvExporter.CsvExporter(inspection_json)
                    inspection_as_list = current_table.convert_audit_to_table()
                    full_inspections.append(inspection_as_list)
                    inspection_as_list = [
                        item
                        for item in inspection_as_list
                        if item[1] not in IGNORED_TYPES
                    ]
                    all_inspections.append(inspection_as_list)
            return all_inspections, full_inspections
        return None, None

    def map_prime_elements(self):
        prime_elements = {}
        list_of_prime_elements = [
            item for item in self.inspection_as_list if item[35] == "primeelement"
        ]
        for pe in list_of_prime_elements:
            prime_elements[pe[14]] = pe[2]
        if prime_elements:
            return prime_elements
        else:
            return None

    def map_item_id_to_label(self):
        checked_dynamicfields = []
        inspection_as_list = self.inspection_as_list
        item_label_map = []
        dict_label_map = OrderedDict()
        count = 0
        for item in inspection_as_list:
            item_id = item[14]
            label = item[2]
            item_type = item[1]
            repeating_section = item[35]
            if item_type == "dynamicfield":
                if item_id not in checked_dynamicfields:
                    rs_map = self.create_rs_map(item_id)
                    checked_dynamicfields.append(item_id)
                    self.csv_as_list.extend(rs_map)
                    continue
            elif repeating_section in [""]:
                count += 1
                rs_check = False
                new_row = {item_id: []}
                self.csv_as_list.append(new_row)
                item_label_map.append(
                    {item_id: {"order": count, "label": label, "rs": rs_check}}
                )
                dict_label_map[item_id] = {
                    "order": count,
                    "label": label,
                    "rs": rs_check,
                    "category": item[34],
                }
            else:
                pass

        return item_label_map, dict_label_map

    def create_rs_map(self, item_id):
        columns = []
        for inspection in self.all_inspections:
            this_inspections_prime_elements = [
                item
                for item in inspection
                if item[35] == "primeelement" and item[1] not in IGNORED_TYPES
            ]
            this_inspections_pe_dict = {}
            for pe in this_inspections_prime_elements:
                this_inspections_pe_dict[pe[2]] = pe[14]
            for row in inspection:
                if row[35] not in ["primeelement", ""]:
                    parent_id = list(row[35])[0]
                    if parent_id == item_id:
                        # section_title = list(row[35])[1]
                        section_number = list(row[35])[2]
                        category = row[34]
                        category = (
                            (category[:50] + "..") if len(category) > 50 else category
                        )
                        question = row[2]
                        question = (
                            (question[:150] + "..") if len(question) > 150 else question
                        )
                        pm_item_id = this_inspections_pe_dict.get(question)
                        new_label = self.prime_elements.get(pm_item_id)
                        if new_label != question:
                            question = new_label
                        new_row_title = f"{category} {section_number} / {question}"
                        new_row = {new_row_title: []}
                        if new_row not in columns:
                            columns.append(new_row)
        return columns


class RowCreator:
    def __init__(self, inspection, template):
        full_combined_inspection = inspection
        self.inspection_as_list = [
            item for item in full_combined_inspection if item[1] not in IGNORED_TYPES
        ]
        self.list_of_prime_elements = [
            item for item in full_combined_inspection if item[35] == "primeelement"
        ]
        self.template_prime_elements = template.prime_elements
        self.csv_as_list = template.csv_as_list
        self.mapped_to_list = self.col_to_value()

    def col_to_value(self):
        list_of_prime_elements = self.list_of_prime_elements
        for row in self.csv_as_list:
            response_found = False
            item_id = list(row.keys())[0]
            if item_id in COMMON_DATA:
                item_values = self.inspection_as_list[0]
                common_data_dict = {
                    "AuditID": item_values[29],
                    "Name": item_values[21],
                    "DateStarted": item_values[26],
                    "DateCompleted": item_values[28],
                    "Percentage Score": item_values[24],
                    "Score": item_values[22],
                    "Site": item_values[42],
                    "Area": item_values[43],
                    "Region": item_values[44],
                    "Template Name": item_values[31],
                }
                common_response = common_data_dict.get(item_id)
                row[item_id].append(common_response)
                response_found = True
            else:
                search_inspection = [
                    item for item in self.inspection_as_list if item[14] == item_id
                ]
                if search_inspection:
                    response = search_inspection[0][3]
                    failed_status = search_inspection[0][12]
                    if failed_status is True:
                        response += FAILED_DELIMITER
                    row[item_id].append(response)
                    response_found = True
                else:
                    for item in self.inspection_as_list:
                        if item[35] not in ["primeelement", ""]:
                            parent_id = list(item[35])[0]
                            section_title = list(item[35])[1]
                            section_number = list(item[35])[2]
                            category = item[34]
                            category = (
                                (category[:50] + "..")
                                if len(category) > 50
                                else category
                            )
                            question = item[2]
                            question = (
                                (question[:150] + "..")
                                if len(question) > 150
                                else question
                            )
                            response = item[3]
                            for primeelement in list_of_prime_elements:
                                if primeelement[2] == question:
                                    if question != self.template_prime_elements.get(
                                        primeelement[14]
                                    ):
                                        question = self.template_prime_elements.get(
                                            primeelement[14]
                                        )
                            new_row_title = f"{category} {section_number} / {question}"
                            if new_row_title == item_id and response_found is False:
                                row[item_id].append(response)
                                response_found = True
            if response_found is False:
                row[item_id].append("")
        return self.csv_as_list


class ConfigSetup:
    def __init__(self):
        self.config = self.get_config()
        self.sp = self.register_token()
        if self.sp:
            self.header_only = self.config["options"]["header_only"]
            self.token = self.config["options"]["token"]
            self.templates = self.get_templates()
            self.file_format = self.config["options"]["export_format"]
            self.to_search = self.config["options"]["to_search"]
            self.parsed_date = self.date_parser()
            self.before, self.after = self.get_dates_to_search()
            self.template_lookup = self.get_formatted_list_of_templates()

    def get_templates(self):
        if self.config["options"]["templates"] is None:
            return []
        elif self.config["options"]["templates"] == "all":
            return "all"
        if "," in str(self.config["options"]["templates"]):
            templates = self.config["options"]["templates"].split(",")
        else:
            templates = [self.config["options"]["templates"]]
        return templates

    @staticmethod
    def get_config():
        config = configparser.ConfigParser()
        config.read("config.ini")
        if config.sections():
            return config
        else:
            return None

    def register_token(self):
        config = self.config
        if self.config:
            if "options" in config:
                if "token" in config["options"]:
                    try:
                        sp = safetypy.SafetyCulture(config["options"]["token"])
                        return sp
                    except:
                        return None
        else:
            return None

    def get_list_of_templates(self):
        list_of_templates = self.sp.discover_templates()
        if list_of_templates is not None:
            list_of_templates = list_of_templates["templates"]
            return list_of_templates
        else:
            return None

    def get_formatted_list_of_templates(self):
        all_templates = self.get_list_of_templates()
        all_templates = sorted(
            all_templates, key=itemgetter("modified_at"), reverse=True
        )
        template_list = [{"value": "all", "name": "Export All", "checked": True}]
        if all_templates:
            for template in all_templates:
                if len(template_list) > 200:
                    break
                else:
                    if template["name"] == "":
                        name = "Untitled template"
                    else:
                        name = template["name"].strip()
                    template_id = template["template_id"]
                    joined_name = f"{name} ({template_id})"
                    template_dict = {
                        "value": template_id,
                        "name": joined_name,
                    }
                    template_list.append(template_dict)
        else:
            template_list = None
        return template_list

    def date_parser(self):
        parsed_date = dateparser.parse(self.to_search)
        if not parsed_date:
            date = search_dates(self.to_search)
        else:
            date = parsed_date
        if not date:
            print(
                "Unable to parse the data in your config file. Defaulting to 1 week ago."
            )
            date = datetime.now() - timedelta(weeks=1)
        return date

    def get_dates_to_search(self):
        before = datetime.now()
        after = before - timedelta(weeks=1)
        if self.parsed_date:
            if type(self.parsed_date) is list:
                for i, date_to_search in enumerate(self.parsed_date):
                    if i == 0:
                        after = date_to_search[1]
                    if i == 1:
                        before = date_to_search[1]
                    if i > 1:
                        print("More than 2 dates provided, only taking the first two.")
            else:
                after = self.parsed_date

            pretty_after_date = after.strftime(f"%A %d %B %Y at %X")
            pretty_after_date = typer.style(
                pretty_after_date, fg=typer.colors.GREEN, bold=True
            )
            pretty_before_date = before.strftime(f"%A %d %B %Y at %X")
            pretty_before_date = typer.style(
                pretty_before_date, fg=typer.colors.GREEN, bold=True
            )
            search_from_alert = (
                f"Searching for inspections completed after "
                f"{pretty_after_date} until {pretty_before_date} "
            )
            typer.echo(search_from_alert)
        else:
            print(
                "Either unable to parse date or no date provided. Defaulting to 1 week ago."
            )
        return before, after
