import html
import json
import numbers

from result_analyser import ResultAnalyser

class AbstractResultsWrapper:
    def to_html_string(self):
        raise NotImplementedError()
    def to_string(self):
        return self.to_html_string()


class ExecutionResultsWrapperOrig(AbstractResultsWrapper):
    def __init__(self, results_json):
        self._results_json = results_json

    def to_html_string(self):
        input = self._results_json
        try:
            data = input['resultSet']
        except KeyError:
            return

        output = """
        <style>
        .dataframe table-wrapper { margin: 2rem 0 !important; }
        .dataframe thead tr:only-child th { text-align: right; }
        .dataframe thead th { text-align: left; }
        .dataframe tbody tr th { vertical-align: top; }
        </style>
        """

        for row_name in data:
            table_content = data[row_name]
            output += "<div class=\"table-wrapper\"><table border=\"1\" class=\"dataframe\">"
            column_labels = input['types'][row_name]
            column_labels_keys = column_labels.keys()

            column_labels_keys = sorted(column_labels_keys)
            column_names = [""]
            for key in column_labels_keys:
                column_names.append(column_labels[key].lower())

            output += "<thead>{}</thead>".format(self.__wrap_as_html_table_row(column_names, "th"))

            for column in table_content:
                output += "<tr><th>{}</th>".format(row_name)
                output += self.__wrap_as_html_table_row(column)
                output += "</tr>"
            output += "</table></div>"

        return output

    def __wrap_as_html_table_row(self, values, element="td"):
        open_tag = "<{}>".format(element)
        close_tag = "</{}>".format(element)

        if len(values) > 1:
            result = open_tag
            result+= "{}{}".format(close_tag, open_tag).join(values)
            result+= close_tag
            return result
        return "{}{}{}".format(open_tag, values[0], close_tag)


class ExecutionExceptionWrapper(AbstractResultsWrapper):
    def __init__(self, error_message):
        self._error_message = error_message

    def to_html_string(self):
        return "<div style='color: red; font-weight: bold'>{}</div>".format(html.escape(self._error_message))


class ResultsDataTableWrapper(AbstractResultsWrapper):
    def __init__(self, data_list, column_names, column_keys, caption, flat_rows=False, single_clmn=False,
             table_class=None, caption_class=None, caption_tooltip = None):
        self._data_list = data_list
        self._column_names = column_names if column_names else None
        self._column_names_pos = {clmn: indx for indx, clmn in enumerate(column_keys)} if column_keys else None
        self._caption = caption
        self._flat_rows = flat_rows
        self._single_clmn = single_clmn
        self._table_class = table_class
        self._caption_class = caption_class
        self._caption_tooltip = caption_tooltip

    def to_html_string(self):
        html_strs = ["<table class=\"{0}\">".format(self._table_class)] if self._table_class else ["<table>"]
        if self._caption:
            html_strs.append(
                "<caption class=\"{0}\"".format(self._caption_class) if self._caption_class else "<caption")
            html_strs.append(
                " title=\"{0}\">".format(html.escape(self._caption_tooltip)) if self._caption_tooltip else ">")
            html_strs.append("{0}</caption>".format(html.escape(self._caption)))
        if self._column_names:
            html_strs.append("<thead><tr>")
            html_strs.extend(["<th>{0}</th>".format(html.escape(cn)) for cn in self._column_names])
            html_strs.append("</tr></thead>")
        aggr_rows = self._process_rows(self._data_list)
        for aggr_row in aggr_rows:
            html_strs.append(self._aggr_row_to_html(aggr_row))
        html_strs.append("</table>")
        return "".join(html_strs)

    def _aggr_row_to_html(self, aggr_row):
        html_strs = ["<tbody>"]
        row_id = 0
        for row in aggr_row:
            html_strs.append( self._row_to_html(row, self._calc_rowspans(row_id, aggr_row)) )
            row_id+=1
        html_strs.append("</tbody>")
        return "".join(html_strs)

    def _val_to_str(self, val):
        if isinstance(val, numbers.Number):
            return '%g' % (val)
        else:
            return str(val)

    def _row_to_html(self, row, rowspans):
        html_strs = ["<tr>"]
        i = 0
        for cell in row:
            if cell is not None:
                if rowspans[i]>1:
                    html_strs.append("<td rowspan=\"{1}\">{0}</td>".format(html.escape(self._val_to_str(cell)), rowspans[i]))
                else:
                    html_strs.append("<td>{0}</td>".format(html.escape(self._val_to_str(cell))))
            i+=1
        html_strs.append("</tr>")
        return "".join(html_strs)

    def _calc_rowspans(self, row_id, rows):
        rowspans = []
        for colmn_id in range(len(rows[0])):
            rowspans.append(self._calc_rowspan(row_id, colmn_id, rows))
        return rowspans

    def _calc_rowspan(self, row_id, colmn_id, rows):
        if rows[row_id][colmn_id] == None:
            return 0
        i = row_id
        for i in range(row_id+1, len(rows)):
            if rows[i][colmn_id] != None:
                i-=1
                break
        return i - row_id + 1


    def _is_complex_list(self, val):
        return isinstance(val, (list, tuple)) and len(val)>0 and isinstance(val[0], dict)

    def _process_rows(self, data_list, pos_indent=0):
        rows = []
        for data in data_list:
            if self._flat_rows:
                if self._single_clmn:
                    rows.append(self._process_row_flat([data]))
                else:
                    assert isinstance(data, (list, tuple))
                    rows.append(self._process_row_flat(data))
            else:
                assert isinstance(data, dict)
                rows.append(self._process_row(data, pos_indent=pos_indent))
        return rows

    def _process_row_flat(self, data_list):
        return [data_list]

    def _process_row(self, data_dict, pos_indent):

        # Check the correctness of parameters
        pos_set = set([self._column_names_pos.get(clmn) for clmn in data_dict.keys()
                   if self._column_names_pos.get(clmn) is not None])
        min_pos = min(pos_set);
        max_pos = max(pos_set);
        assert min_pos == pos_indent
        assert max_pos - min_pos + 1 == len(pos_set)

        data_vals = [val for _, val in sorted(data_dict.items(),
                key=lambda itm: self._column_names_pos.get(itm[0])
                if self._column_names_pos.get(itm[0]) is not None else len(data_dict) + pos_indent )]

        for val in data_vals:
            assert val is not None

        basis_row = []
        i=0
        for val in data_vals:
            if not self._is_complex_list(val):
                basis_row.append(val)
                i+=1
            else:
                break

        assert i >= len(data_vals)-1 # only 1 complex value (dict) is allowed

        aggr_row = []
        if i == len(data_vals):
            aggr_row = [basis_row]
        else:
            assert self._is_complex_list(data_vals[i])
            nested_aggr_rows = self._process_rows(val, pos_indent=pos_indent+len(basis_row))
            j = 0
            for nested_aggr_row in nested_aggr_rows:
                for nested_row in nested_aggr_row:
                    if j == 0:
                        aggr_row.append( basis_row + nested_row)
                    else:
                        aggr_row.append( [None]*len(basis_row) + nested_row)
                j+=1
        return aggr_row


class ExecutionResultsWrapper(AbstractResultsWrapper):
    def __init__(self, results_json):
        self._result_set = results_json['resultSet']
        self._types_set = results_json['columnNames']
        self._elapsed_time_ms = results_json.get('elapsedTimeMs', None)
        self._table_class = "vadalog-300px-min-wide-table"
        self._caption_class = "vadalog-table-caption"


    def to_html_string(self):
        html_strs = [ALL_CSS_STYLE]
        html_strs.append("<div class=\"vadalog-output\">")
        
        # Display elapsed time at the top if available
        if self._elapsed_time_ms is not None:
            elapsed_seconds = self._elapsed_time_ms / 1000.0
            html_strs.append("""
                <div style='
                    margin-bottom: 16px; 
                    padding: 12px 16px; 
                    background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
                    border-left: 4px solid #4CAF50; 
                    border-radius: 8px;
                    box-shadow: 0 2px 8px rgba(76, 175, 80, 0.15);
                    font-family: "JetBrains Mono", monospace;
                    font-size: 14px;
                    color: #2e7d32;
                    display: flex;
                    align-items: center;
                    gap: 8px;
                '>
                    <span style='font-size: 20px;'>⏱️</span>
                    <span style='font-weight: 500;'>Execution time:</span>
                    <strong style='font-size: 16px; color: #1b5e20;'>{:.2f}s</strong>
                </div>
            """.format(elapsed_seconds))
        
        for pred, facts in self._result_set.items():
            clmn_names = self._types_set.get(pred, [""] * (len(facts[0]) if len(facts)>0 else 0) );
            # clmn_names = [ clmn_name.lower() for _, clmn_name
            #               in sorted(self._types_set[pred].items(), key=lambda itm: itm[0]) ]
            html_strs.append("<div class=\"vadalog-output-block-inline-scroll\">")
            html_strs.append("<div class=\"vadalog-output-table-wrap\">")
            w = ResultsDataTableWrapper(facts,
                clmn_names, None, pred, flat_rows=True,
                table_class = self._table_class, caption_class = self._caption_class)

            html_strs.append(w.to_html_string())
            html_strs.append("</div>") # vadalog-output-table-wrap
            html_strs.append("</div>") # vadalog-output-block-inline-scroll
        html_strs.append("</div>")
        return "".join(html_strs)


class ModuleExecutionResultsWrapper(AbstractResultsWrapper):
    def __init__(self, results_json):
        self._results = results_json['results']
        self._column_names = [ ["parameterNames", "Parameters"], ["tuple", "Values"], ["status", "Status"], ["errorMessage", "Error message"] ]
        self._table_class = "vadalog-wide-table"
        self._caption_class = "run-module-result-caption"

    def to_html_string(self):
        html_strs = [ALL_CSS_STYLE]
        html_strs.append("<div class=\"vadalog-output\">")

        all_params = []
        for result in self._results:
            params = {}
            for name_pair in self._column_names:
                val = result.get(name_pair[0], " ")
                if name_pair[0] == "status":
                    if val == "SUCCESS":
                        analysis_res = ResultAnalyser(result.get("jsonReply", "{}")).analyse()
                        if analysis_res["status"] == "SUCCESS":
                            params["status"] = "SUCCESS"
                            params["errorMessage"] = None
                        else:
                            params["status"] = "FAILURE"
                            params["errorMessage"] = analysis_res["error_msg"]
                    else:
                        params["status"] = "FAILURE"
                        params["errorMessage"] = result.get("errorMessage", None)
                elif name_pair[0] != "errorMessage":
                    params[name_pair[0]] = val

            all_params.append( [params[clmn] if params[clmn] else " " for clmn,_ in self._column_names] )

        clmn_names = [ name for _, name in self._column_names ]
        html_strs.append("<div class=\"vadalog-output-block-inline-scroll\">")
        html_strs.append("<div class=\"vadalog-output-table-wrap\">")
        w = ResultsDataTableWrapper(all_params,
            clmn_names, None, "Module execution results", flat_rows=True,
            table_class = self._table_class, caption_class = self._caption_class)

        # w = ResultsDataTableWrapper(data,
        #     self._clmn_names.get(code), None, metadata["message"],
        #     flat_rows=True, single_clmn=code in self._force_single_clmn,
        #     table_class=self._table_class, caption_class=self._caption_classes.get(metadata["resultClass"]),
        #     caption_tooltip = metadata["resultClass"])

        html_strs.append(w.to_html_string())
        html_strs.append("</div>") # vadalog-output-table-wrap
        html_strs.append("</div>") # vadalog-output-block-inline-scroll
        html_strs.append("</div>")
        return "".join(html_strs)


class CodeAnalysisResultsWrapper(AbstractResultsWrapper):
    def __init__(self, results_json):
        self._all_metadata = results_json["metadata"]
        self._all_data = results_json["data"]
        self._table_class = "vadalog-wide-table"

        self._no_data_results = set(["NoOutputAnalysis"])

        self._list_results = set([
            "InputOutputSamePredicateAnalysis",
            "DanglingOutputAnnotationAnalysis",
            "OutputAnnotationMultiBindAnalysis",
            "DisconnectedBodyAnalysis",
            "DanglingBindAnnotationAnalysis",
            "DanglingHeadPredicatesAnalysis",
            "DanglingBodyPredicatesAnalysis",
            "PredicateMultipleArityAnalysis"])

        self._complex_results = set([
            "ExistentialVariablesInHeadAnalysis",
            "UnboundVariablesInExpressionLhsToHeadAnalysis",
            "UnboundVariablesInExpressionRhsAnalysis",
            "NonWardedAnalysis",
            "DangerousHarmfulVarsInWardedRulesAnalysis"])

        self._clmn_names = {
            "InputOutputSamePredicateAnalysis": ["Predicate"],
            "DanglingOutputAnnotationAnalysis": ["Annotation"],
            "OutputAnnotationMultiBindAnalysis": ["Predicate"],
            "DisconnectedBodyAnalysis": ["Rule"],
            "DanglingBindAnnotationAnalysis": ["Annotation"],
            "DanglingHeadPredicatesAnalysis": ["Predicates"],
            "DanglingBodyPredicatesAnalysis": ["Predicates"],
            "PredicateMultipleArityAnalysis": ["Predicate", "Arities"],
            "ExistentialVariablesInHeadAnalysis": ["Rule", "Variables"],
            "UnboundVariablesInExpressionLhsToHeadAnalysis": ["Rule", "Condition", "Variable"],
            "UnboundVariablesInExpressionRhsAnalysis": ["Rule", "Condition", "Variables"],
            "NonWardedAnalysis": ["Rule", "Potential Wards", "Shared Dangerous Variables", "Dangerous Variables"
                , "Shared Harmful Variables", "Harmful Variables", "Problem"],
            "DangerousHarmfulVarsInWardedRulesAnalysis": ["Rule", "Ward", "Dangerous Variables", "Harmful Variables"]
        }

        self._force_single_clmn = set([
            "InputOutputSamePredicateAnalysis",
            "DanglingOutputAnnotationAnalysis",
            "OutputAnnotationMultiBindAnalysis",
            "DisconnectedBodyAnalysis",
            "DanglingBindAnnotationAnalysis",
            "DanglingHeadPredicatesAnalysis",
            "DanglingBodyPredicatesAnalysis"
        ])

        self._clmn_keys = {
            "ExistentialVariablesInHeadAnalysis": ["rule", "variables"],
            "UnboundVariablesInExpressionLhsToHeadAnalysis": ["rule", "condition", "variable"],
            "UnboundVariablesInExpressionRhsAnalysis": ["rule", "condition", "variables"],
            "NonWardedAnalysis": ["rule", "potential_wards", "shared_dangerous_variables", "dangerous_variables"
                , "shared_harmful_variables", "harmful_variables", "problem"],
            "DangerousHarmfulVarsInWardedRulesAnalysis": ["rule", "ward", "dangerous_variables", "harmful_variables"]
        }

        self._caption_classes = {
            "Warning": "vadalog-code-analysis-warning",
            "Error": "vadalog-code-analysis-error",
            "Note": "vadalog-code-analysis-note"
        }

        self._error_classes_sort = {
            "Warning": 2,
            "Error": 1,
            "Note": 3
        }

    def _single_message(self, aclass, atooltip, msg):
        return "<div class=\"vadalog-output-no-data-wide {0}\" title=\"{1}\">{2}</div>"\
                    .format(aclass, atooltip, msg)

    def to_html_string(self):
        html_strs = [ALL_CSS_STYLE]
        html_strs.append("<div class=\"vadalog-output\">")

        all_metadata_sorted = sorted(self._all_metadata.items(),
             key = lambda item: ( self._error_classes_sort.get(item[1]["resultClass"], 1000), item[0]))

        problems_cnt = 0
        for code, metadata in all_metadata_sorted:
            if metadata["status"] == False:


                if code in self._no_data_results:
                    html_strs.append("<div class=\"vadalog-output-block\">")
                    html_strs.append( self._single_message(
                            self._caption_classes.get(metadata["resultClass"], ""),
                            metadata["resultClass"], metadata["message"]) )
                    html_strs.append("</div>")  # vadalog-output-table-wrap
                    html_strs.append("</div>")  # vadalog-output-block
                else:
                    html_strs.append("<div class=\"vadalog-output-block-scroll\">")
                    html_strs.append("<div class=\"vadalog-output-table-wrap\">")
                    if code in self._list_results:
                        data = self._all_data.get(code)
                        if data is not None:
                            w = ResultsDataTableWrapper(data, self._clmn_names.get(code), None, metadata["message"],
                                flat_rows=True, single_clmn=code in self._force_single_clmn,
                                table_class=self._table_class, caption_class=self._caption_classes.get(metadata["resultClass"]),
                                caption_tooltip = metadata["resultClass"])
                            html_strs.append(w.to_html_string())
                        else:
                            html_strs.append( self._single_message(
                                self._caption_classes.get(metadata["resultClass"], ""),
                                metadata["resultClass"], metadata["message"]) )
                    elif code in self._complex_results:
                        data = self._all_data.get(code)
                        if data is not None:
                            w = ResultsDataTableWrapper(data, self._clmn_names.get(code), self._clmn_keys.get(code),
                            metadata["message"], flat_rows=False,
                            table_class=self._table_class, caption_class=self._caption_classes.get(metadata["resultClass"]),
                            caption_tooltip=metadata["resultClass"])
                            html_strs.append(w.to_html_string())
                        else:
                            html_strs.append( self._single_message(
                                self._caption_classes.get(metadata["resultClass"], ""),
                                metadata["resultClass"], metadata["message"]) )
                    html_strs.append("</div>") # vadalog-output-table-wrap
                    html_strs.append("</div>") # vadalog-output-block

                problems_cnt+=1

        if problems_cnt == 0:
            html_strs.append(self._single_message(
                "vadalog-code-analysis-no-error", "Note", "No problems identified in the code"))

        html_strs.append("</div>")
        return "".join(html_strs)


class MetaProgramResultsWrapper(AbstractResultsWrapper):
    def __init__(self, results_str):
        self._results_str = results_str

    def to_html_string(self):
        html_strs = [ALL_CSS_STYLE]
        html_strs.append("<div class=\"vadalog-output\">")
        for key in self._results_str:
            html_strs.append("<div class=\"vadalog-output-block\">")
            html_strs.append("<div class=\"vadalog-output-header\">")
            html_strs.append(key)
            html_strs.append("</div>") # vadalog-output-header
            html_strs.append("<div class=\"vadalog-output-header\">")
            html_strs.append("<textarea rows=\"10\" class=\"vadalog-meta-program-textarea\">")
            html_strs.append(self._results_str[key])
            html_strs.append("</textarea>") # vadalog-output-table-wrap
            html_strs.append("</div>") # vadalog-output-block
        html_strs.append("</div>")
        return "".join(html_strs)

class ExplainResultsWrapper(AbstractResultsWrapper):
    def __init__(self, results_json):
        self._result_set = results_json['resultSet']

    @staticmethod
    def get_node_style(nodeType):
        if nodeType == "AND":
            return " shape = rectangle style = filled fillcolor = moccasin "
        else:
            return " shape = ellipse style = filled fillcolor =  lightcyan2 "

    def to_string(self):
        selection = {"explain:node": self._result_set["explain:node"], "explain:edge": self._result_set["explain:edge"]}
        return json.dumps(selection)


# /* https://css-tricks.com/complete-guide-table-element/ */
ALL_CSS_STYLE ="""
    <style>
/* === TABLE === v2.0 - Beautiful Modern Tables === */

.vadalog-output {
    margin-top: 8px;
    width: 100%;
    vertical-align: text-top;
}

.vadalog-output-block {
	display: block;
}

.vadalog-output-block-scroll {
    .vadalog-output-block
	overflow-x: auto;
}

.vadalog-output-block-inline-scroll {
	display: inline-block;
	margin-right:8px;
	overflow-x: auto;
	vertical-align: text-top;
}

.vadalog-output-table-wrap {
	display: table;
}

.vadalog-output-no-data-wide {
    text-align: left;
    padding: 4px 8px 4px 8px;
    font-weight: bold;
    font-size: 12px;
    margin-bottom: 8px;
    margin-top: 8px;
    width:auto;
    overflow-x: auto;
}

.vadalog-wide-table {
	width:100%;
}

.vadalog-300px-min-wide-table {
	min-width: 300px;
}

.vadalog-output-table-wrap table {
    border-collapse: collapse;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    border-radius: 8px;
    overflow: hidden;
}
.vadalog-output-table-wrap table caption {
    display: table-caption;
    text-align: left;
    padding: 0 8px 4px 8px;
    font-weight: bold;
    font-size: 13px;
    font-family: 'JetBrains Mono', monospace;
}

.vadalog-output-table-wrap table td {
    border: 1px solid rgb(230, 230, 230);
    text-align: left;
    color: rgb(51, 51, 51) !important;
    padding: 8px 12px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 13px;
    transition: background-color 0.2s ease;
}

.vadalog-output-table-wrap table th {
    border: 1px solid rgb(230, 230, 230);
    background-color: #e9ecef !important;
    text-align: left;
    padding: 8px 12px;
    font-family: 'JetBrains Mono', monospace;
    font-weight: 700 !important;
    text-transform: uppercase;
    font-size: 12px;
    letter-spacing: 0.8px;
    color: #2c3e50 !important;
    border-bottom: 2px solid #dee2e6 !important;
}

.vadalog-output-table-wrap table thead th {
    font-weight: 700 !important;
    background-color: #e9ecef !important;
}

.vadalog-output-table-wrap table tr {
	background-color: rgba(0,0,0,0)!important;
}

.vadalog-output-table-wrap table tbody:nth-child(odd) tr td {
    background-color: white !important;
}
.vadalog-output-table-wrap table tbody:nth-child(even) tr td {
    background-color: rgb(249, 250, 251) !important;
}
.vadalog-output-table-wrap table tbody tr:hover td {
    background-color: #e3f2fd !important;
    cursor: pointer;
    transform: scale(1.01);
}

.vadalog-output-table-wrap table th {
    text-align: left;
}

/* === RUN MODULE === */
.run-module-result-caption {
    color: rgb(56, 61, 65);
    background-color: rgb(226, 227, 229);
    border: 1px solid rgb(214, 216, 219);
}

.module-run-result-success {
    color: rgb(21, 87, 36);
}
.module-run-result-unsuccess {
    color: rgb(114, 28, 36);
}

/* === CODE ANALYSIS === */
.vadalog-code-analysis-warning {
    color: rgb(133, 100, 4);
	background-color: rgb(255, 243, 205);
	border: 1px solid rgb(255, 238, 186);
}
.vadalog-code-analysis-error {
    color: rgb(114, 28, 36);
	background-color: rgb(248, 215, 218);
	border: 1px solid rgb(245, 198, 203);
}
.vadalog-code-analysis-note {
    color: rgb(12, 84, 96);
    background-color: rgb(209, 236, 241);
    border: 1px solid rgb(190, 229, 235);
}
.vadalog-code-analysis-no-error {
    color: rgb(21, 87, 36);
    background-color: rgb(212, 237, 218);
    border: 1px solid rgb(195, 230, 203);
}

.vadalog-table-caption {
    color: rgb(0, 64, 133);
    background: linear-gradient(135deg, rgb(204, 229, 255) 0%, rgb(184, 218, 255) 100%);
    border: 1px solid rgb(184, 218, 255);
    border-radius: 6px 6px 0 0;
    box-shadow: 0 2px 4px rgba(0, 64, 133, 0.1);
}

/* === META PROGRAM === */

.vadalog-output-block textarea.vadalog-meta-program-textarea {
    width: 95%;
    white-space: pre;
    overflow: auto;
    min-height: 80px;
}
</style>
"""
