import csv
import inspect
import os
import warnings
from typing import Any
from typing import Never

import regex
from dotenv import load_dotenv

from dsp_tools.error.xmllib_errors import XmllibFileNotFoundError
from dsp_tools.error.xmllib_errors import XmllibInputError
from dsp_tools.error.xmllib_warnings import MessageInfo
from dsp_tools.error.xmllib_warnings import UserMessageSeverity
from dsp_tools.error.xmllib_warnings import XmllibInputInfo
from dsp_tools.error.xmllib_warnings import XmllibInputWarning
from dsp_tools.utils.ansi_colors import BOLD_YELLOW
from dsp_tools.utils.ansi_colors import RESET_TO_DEFAULT

load_dotenv()


def initialise_warning_file() -> None:
    if file_path := os.getenv("XMLLIB_WARNINGS_CSV_SAVEPATH"):
        try:
            new_row = ["File", "Severity", "Message", "Resource ID", "Property", "Field"]
            with open(file_path, "w", newline="") as file:
                print(
                    BOLD_YELLOW,
                    f"Warnings generated by the xmllib "
                    f"will be saved to '{file_path}' and will not be printed in the terminal.",
                    RESET_TO_DEFAULT,
                )
                writer = csv.writer(file)
                writer.writerow(new_row)
        except FileNotFoundError:
            raise XmllibFileNotFoundError(
                f"The filepath '{file_path}' you entered in your .env file does not exist. "
                f"Please ensure that the folder you named exists."
            ) from None


def write_message_to_csv(
    file_path: str, msg: MessageInfo, function_trace: str | None, severity: UserMessageSeverity
) -> None:
    new_row = [
        function_trace if function_trace else "",
        str(severity),
        msg.message if msg.message else "",
        msg.resource_id if msg.resource_id else "",
        msg.prop_name if msg.prop_name else "",
        msg.field if msg.field else "",
    ]
    with open(file_path, "a", newline="") as file:
        writer = csv.writer(file)
        writer.writerow(new_row)


def get_user_message_string(msg: MessageInfo, function_trace: str | None) -> str:
    str_list = [f"File '{function_trace}'"] if function_trace else []
    if msg.resource_id:
        str_list.append(f"Resource ID '{msg.resource_id}'")
    if msg.prop_name:
        str_list.append(f"Property '{msg.prop_name}'")
    if msg.field:
        str_list.append(f"Field '{msg.field}'")
    str_list.append(msg.message)
    return " | ".join(str_list)


def _get_calling_code_context() -> str | None:
    """
    Find file name and line number of the file that was written by the user.
    """
    all_stack_frames = inspect.stack()
    frame_files = [x.filename for x in all_stack_frames]
    calling_func_index = _get_stack_frame_number(frame_files)
    if calling_func_index == 0:
        return None
    user_frame_info = all_stack_frames.pop(calling_func_index)
    file_name = user_frame_info.filename.rsplit("/", 1)[1]
    return f"{file_name}:{user_frame_info.lineno}"


def _get_stack_frame_number(file_names: list[str]) -> int:
    """
    Get index number of first python file of the stack trace which is not any more in the dsp-tools code.
    This is the index of the first user python file.
    """
    calling_func_index = -1
    for file in file_names:
        if _filter_stack_frames(file):
            calling_func_index += 1
        else:
            calling_func_index += 1
            break
    return calling_func_index


def _filter_stack_frames(file_path: str) -> bool:
    dsp_tools_path = r"\/dsp[-_]tools\/(xmllib|error)\/"
    if regex.search(dsp_tools_path, file_path):
        return True
    elif regex.search(r"^<[a-zA-Z]+>$", file_path):
        # This is for functions like str(), which appear in the stack trace as filename "<string>"
        return True
    return False


def raise_xmllib_input_error(msg: MessageInfo) -> Never:
    """These are to be used if the error is caused by user input."""
    function_trace = _get_calling_code_context()
    if file_path := os.getenv("XMLLIB_WARNINGS_CSV_SAVEPATH"):
        write_message_to_csv(file_path, msg, function_trace, UserMessageSeverity.ERROR)
    msg_str = get_user_message_string(msg, function_trace)
    raise XmllibInputError(msg_str)


def emit_xmllib_input_warning(msg: MessageInfo) -> None:
    """These are to be used if the error is caused by user input."""
    function_trace = _get_calling_code_context()
    if file_path := os.getenv("XMLLIB_WARNINGS_CSV_SAVEPATH"):
        write_message_to_csv(file_path, msg, function_trace, UserMessageSeverity.WARNING)
    else:
        msg_str = get_user_message_string(msg, function_trace)
        warnings.warn(XmllibInputWarning(msg_str))


def emit_xmllib_input_info(msg: MessageInfo) -> None:
    """These are to be used if the error is caused by user input."""
    function_trace = _get_calling_code_context()
    if file_path := os.getenv("XMLLIB_WARNINGS_CSV_SAVEPATH"):
        write_message_to_csv(file_path, msg, function_trace, UserMessageSeverity.INFO)
    else:
        msg_str = get_user_message_string(msg, function_trace)
        warnings.warn(XmllibInputInfo(msg_str))


def emit_xmllib_input_type_mismatch_warning(
    *,
    expected_type: str,
    value: Any,
    res_id: str | None,
    value_field: str | None = None,
    prop_name: str | None = None,
) -> None:
    """These are to be used if the error is caused by user input."""
    msg_info = MessageInfo(
        message=f"The input should be a valid {expected_type}, your input '{value}' does not match the type.",
        resource_id=res_id,
        prop_name=prop_name,
        field=value_field,
    )
    emit_xmllib_input_warning(msg_info)
