import uuid
from copy import deepcopy
from importlib.resources import files
from typing import Any

from .. import RELAIData
from ..data import DataType
from ...utils import format_title
from .feedback_questions import FeedbackQuestions


def format_row_value(value: Any) -> str:
    """Makes the row value more readable.

    Converts lists and dictionaries to a string representation.

    Args:
        value (Any): The value to format.

    Returns:
        str: The formatted value.
    """
    if isinstance(value, list):
        return "\n".join(map(str, value))
    elif isinstance(value, dict):
        return "\n".join(f"{k}: {v}" for k, v in value.items())
    return value


class Table(RELAIData):
    """Data model for a table component.

    Args:
        title (str): Title of the table.
        columns (list[Any]): List of columns. Each column must follow the following format:
            {
                "name": str,
                "type": `DataType` object,
                "hideInTable": bool (optional, default: False),
                "showInModal": bool (optional, default: True),
            }
        rows (list[Any]): List of rows. Each row is a dictionary with keys corresponding to column names.
        accepts_feedback (bool, optional): Whether this component accepts feedback. Defaults to False.
        feedback_questions (list[Any], optional): List of feedback questions. Defaults to None.
            Each feedback question must follow the following format:
                {
                    "question": str,
                    "type": `DataType` object,
                    "context": A list of RELAIData components to show as context for the question. Required if the
                        type is "contextSelection",
                    "forRow": int (0-based row index for which the question is asked),
                    "forColumn": int (0-based column index for which the question is asked). Can be `None`
                        if the question is not specific to a column,
                }
        column_span (int, optional): Number of columns the component should span. Defaults to 12.
    """

    schema_file: str = files("relai_visualization").joinpath("schema/components/table.schema.json")

    def __init__(
        self,
        title: str,
        columns: list[Any],
        rows: list[Any],
        accepts_feedback: bool = False,
        feedback_questions: list[Any] = None,
        column_span: int = 12,
    ) -> None:
        if accepts_feedback:
            if not feedback_questions:
                raise ValueError("Feedback questions must be provided if feedback is accepted.")
            if len(feedback_questions) < 1:
                raise ValueError("At least one feedback question must be provided.")

        formatted_rows = []
        columns = deepcopy(columns)

        # Add unique IDs to columns and format their names
        for column in columns:
            if "rowId" in column:
                raise ValueError("'rowId' field is reserved and not allowed.")
            column["columnId"] = f"column-{uuid.uuid4().hex}"
            column["name"] = format_title(column["name"])

        # Add unique IDs to rows and format their values
        for row in rows:
            new_row = {}
            for k, v in row.items():
                new_row[format_title(k)] = format_row_value(v)
            new_row["rowId"] = f"row-{uuid.uuid4().hex}"
            formatted_rows.append(new_row)
        rows = formatted_rows

        rowwise_feedback_questions = None
        if feedback_questions:
            for feedback_question in feedback_questions:
                try:
                    if columns[feedback_question["forColumn"]]["type"] != feedback_question["type"]:
                        raise ValueError("Feedback question type must match the type of the column it modifies.")
                except IndexError:
                    raise ValueError("Invalid 'forColumn' index in feedback question.")
                except TypeError:
                    # If forColumn is None, the question is not specific to a column
                    pass
            rowwise_feedback_questions = [[] for _ in range(len(rows))]
            for feedback_question in feedback_questions:
                try:
                    if not 0 <= feedback_question["forRow"] < len(rows):
                        raise ValueError("Invalid 'forRow' index in feedback question.")
                except KeyError:
                    raise ValueError("Feedback question must have a 'forRow' field.")
                rowwise_feedback_questions[feedback_question["forRow"]].append(
                    {
                        "question": feedback_question["question"],
                        "type": feedback_question["type"],
                        "context": feedback_question.get("context"),
                        "modifies": [
                            rows[feedback_question["forRow"]]["rowId"],
                        ],
                    }
                )
                # If the feedback question is specific to a column, add the column ID to the modifies list
                if feedback_question["forColumn"] is not None:
                    rowwise_feedback_questions[feedback_question["forRow"]][-1]["modifies"].append(
                        columns[feedback_question["forColumn"]]["columnId"]
                    )
            rowwise_feedback_questions = [
                FeedbackQuestions(row_questions).data for row_questions in rowwise_feedback_questions
            ]

        for column in columns:
            column["type"] = column["type"].data

        super().__init__(
            componentName="table",
            title=title,
            columnSpan=column_span,
            columns=columns,
            rows=rows,
            acceptsFeedback=accepts_feedback,
            feedbackQuestions=rowwise_feedback_questions,
        )

    def custom_validation(self) -> None:
        for row in self.data["rows"]:
            if set(row.keys()) - {"rowId"} != set(c["name"] for c in self.data["columns"]):
                raise ValueError("Column names and row fields must match.")
            for column in self.data["columns"]:
                for row in self.data["rows"]:
                    value = row[column["name"]]
                    # Check if the value is of valid type, as long as it is not None
                    if value and not DataType.from_dict(column["type"]).validate(value):
                        raise ValueError(f"Invalid value '{value}' for column '{column['name']}'")

    @classmethod
    def user_feedback(cls, component: dict) -> list[dict]:
        """Get the user feedback for the feedback questions.

        Args:
            component (dict): Table component data as a dictionary.

        Returns:
            dict: User feedback for the feedback questions as a list of dictionaries mapping question to feedback values.
                Each item in the list corresponds to a row in the table in the same order.
        """
        output = [{} for _ in range(len(component["feedbackQuestions"]))]
        for idx, row_questions in enumerate(component["feedbackQuestions"]):
            for question_id, answer in row_questions["feedback"].items():
                output[idx][row_questions["feedbackQuestions"][question_id]["question"]] = answer
        return output

    @classmethod
    def update_with_feedback(cls, component: dict, modifies: list[str], new_value: Any) -> dict:
        """Update a `Table` component with user feedback.

        Args:
            component (dict): `Table` component to update as a Python dictionary.
            modifies (list[str]): QuestionId, RowId and ColumnId to modify.
            new_value (Any): New value to set.

        Returns:
            dict: Updated `Table` component.
        """
        component = deepcopy(component)
        try:
            question_id, row_id, column_id = modifies
        except ValueError:
            # column_id is missing
            question_id, row_id = modifies
            column_id = None

        # Find the row index and update the value
        for idx, row in enumerate(component["rows"]):
            if row["rowId"] == row_id:
                row_idx = idx
                break
        else:
            raise ValueError("Invalid row ID.")

        if column_id is not None:
            # Do this only if the feedback is for a specific column
            # Find the column name and index
            for idx, column in enumerate(component["columns"]):
                if column["columnId"] == column_id:
                    column_name = column["name"]
                    column_idx = idx
                    break
            else:
                raise ValueError("Invalid column ID.")

            # Check if the value is of valid type, as long as it is not None
            if new_value and not DataType.from_dict(component["columns"][column_idx]["type"]).validate(new_value):
                raise ValueError(f"Invalid value '{new_value}' for column {column_idx}.")

            # Update the value
            component["rows"][row_idx][column_name] = new_value

        # Update the FeedbackQuestions component with the new feedback value as well
        component["feedbackQuestions"][row_idx] = FeedbackQuestions.update_with_feedback(
            component=component["feedbackQuestions"][row_idx],
            modifies=modifies,
            new_value=new_value,
        )
        return component
