from dataclasses import dataclass
from typing import Optional

import requests
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QProgressBar
from qgis.core import Qgis, QgsMapLayer, QgsProject, QgsWkbTypes, QgsSymbol, QgsLayerTreeLayer, QgsLayerTreeGroup, \
    QgsSettings, QgsApplication, QgsMessageLog
from qgis.gui import QgsMessageBar
from qgis.utils import iface

from .color_matching import Matcher
from .embedder import Embedder
from .name_matching import *

DEFAULT_ICON_SIZE = 2
USE_AI = True


@dataclass
class VectorLayer:
    qgs_layer: QgsMapLayer.VectorLayer
    qgs_layer_id: str
    suggested_symbol: Optional[QgsSymbol] = None
    suggested_colors: Optional[list[QColor]] = None
    suggested_icons: Optional[list[str]] = None
    embedding: Optional[list[float]] = None
    normalized_name: Optional[str] = None

    is_default_symbol: Optional[bool] = False
    suggested_icon_path: Optional[str] = None
    checked: Optional[bool] = True

    @property
    def name(self):
        return self.qgs_layer.name()

    @property
    def geo_type_int(self):
        return self.qgs_layer.geometryType()


    @property
    def geo_type_str(self):
        geo_type_map = {
            Qgis.GeometryType.PointGeometry: "Point",
            Qgis.GeometryType.LineGeometry: "Line",
            Qgis.GeometryType.PolygonGeometry: "Polygon",
            Qgis.GeometryType.UnknownGeometry: "Unknown",
            Qgis.GeometryType.NullGeometry: "None"
        }
        return geo_type_map.get(self.geo_type_int, "Unknown")

    @classmethod
    def get_all_from_qgs_project(cls):
        project = QgsProject.instance()
        all_ql_layers = get_ordered_layers(project.layerTreeRoot())

        vl_objects = [cls(qgs_layer=ql, qgs_layer_id=ql.id()) for ql in all_ql_layers if is_supported_layer(ql)]
        
        if not vl_objects:  # No supported vector layers found
            indicate_error("No vector layers found in the project!", Qgis.Warning)
            return []

        symbolize(vl_objects)

        return vl_objects

    @classmethod
    def add_layers_after_loading(cls, q_layers):
        vl_objects = [cls(qgs_layer=ql, qgs_layer_id=ql.id()) for ql in q_layers if is_supported_layer(ql)]
        symbolize(vl_objects)
        return vl_objects

    def change_checked(self, checked: bool):
        self.checked = checked


def symbolize(layers: list):
    if len(layers) <= 0:
        return

    normalize_name(layers)

    pb = create_progress_bar()
    mb = create_msg_progress_bar(pb)
    settings = QgsSettings()
    api_key = settings.value("/plugins/AIAMAS/api_key", "", str)
    language = "English"
    embedder = Embedder(api_key=api_key, layers=layers)
    matcher = Matcher(api_key=api_key, language=language)

    if api_key.strip() not in [None, ""] and USE_AI:
        step = 50 / len(layers)
    else:
        step = 100 / len(layers)

    embedder.step_signal.connect(lambda: up_progress(pb, step))
    embedder.load_embedding_cache()
    embed_if_ai(embedder=embedder, api_key=api_key)

    matcher.step_signal.connect(lambda: up_progress(pb, (step / 2)))
    matcher.match(layers=layers)

    delete_msg_progress_bar(pb, mb)


def embed_if_ai(embedder: Embedder, api_key):
    invalid_token_message = "The API token is invalid. Enter a valid one to use LLMHub (by IFS OST). Otherwise, algorithmic string similarity will always be used."
    token_expired_message = "The API token has expired. Get a new one to use LLMHub (by IFS OST). Otherwise algorithmic string similarity will always be used."
    limit_exceeded_message = "The API token limit has been reached. Algorithmic string similarity is used instead."
    llmhub_error_message = "There is a problem accessing LLMHub (by IFS OST). The plugin now falls back to algorithmic string similarity. Please try again later."
    no_internet_connection_message = "There is a problem with the Internet connection. Please check your Internet settings. In the meantime, the plugin falls back to algorithmic string similarity."

    if api_key.strip() not in [None, ""] and USE_AI:  # Check if the API key is not empty or None
        if is_internet_connection_available(): # Check if connected to internet
            if is_llmhub_available():  # Check if the llmhub service is available
                try:
                    embedder.name_embedder()  # Embed the names of the layers
                except requests.exceptions.RequestException as e:
                    try:
                        json_response = e.response.json().get("detail")
                    except ValueError: # encompasses both requests.exceptions.JSONDecodeError and simplejson.errors.JSONDecodeError
                        json_response = "not valid json in response"
                    if e.response.status_code == 401 and json_response == "Missing or invalid API key": # Handle specific HTTP error 401 and json response indicating invalid API key
                        indicate_error(invalid_token_message, Qgis.Critical)
                    elif e.response.status_code == 401 and json_response == "Token expired": # Handle specific HTTP error 401 and json response indicating expired API key
                        indicate_error(token_expired_message, Qgis.Critical)
                    elif e.response.status_code == 429: # Handle specific HTTP error 429 indicating API key limit has been exceeded
                        indicate_error(limit_exceeded_message, Qgis.Critical)
                    else: # Handle other exceptions related to connection or processing errors
                        indicate_error(llmhub_error_message, Qgis.Critical)
                except Exception as e: # Handle other exceptions related to connection or processing errors
                    indicate_error(llmhub_error_message, Qgis.Critical)
            else:  # Inform the user about the unavailability of the llmhub service
                indicate_error(llmhub_error_message, Qgis.Critical)
        else: # Inform the user that the internet connection is not available
            indicate_error(no_internet_connection_message, Qgis.Critical)   

def indicate_error(message, level):
    create_msg_bar(message, level)

def create_msg_bar(msg, level=Qgis.Info):
    bar = iface.messageBar()
    widget = bar.createMessage("AIAMAS", msg)
    bar.pushWidget(widget, level, 7)


def get_ordered_layers(group):
    layers = []
    for child in group.children():
        if isinstance(child, QgsLayerTreeLayer):
            layers.append(child.layer())
        elif isinstance(child, QgsLayerTreeGroup):
            layers.extend(get_ordered_layers(child))
    return layers


def is_true_vectorlayer(qgis_layer):
    return qgis_layer.type() == QgsMapLayer.VectorLayer and qgis_layer.isSpatial()


def is_supported_layer(qgis_layer):
    if is_true_vectorlayer(qgis_layer):
        return qgis_layer.geometryType() in {Qgis.GeometryType.PointGeometry, Qgis.GeometryType.LineGeometry, Qgis.GeometryType.PolygonGeometry}



def is_internet_connection_available():
    try:
        requests.get("https://www.google.com/", timeout=5)
        return True
    except requests.ConnectionError:
        return False

def is_llmhub_available():
    try:
        requests.get("https://llmhub.infs.ch/", timeout=5)
        return True
    except requests.ConnectionError:
        return False


def delete_msg_progress_bar(pb: QProgressBar, message_widget):
    pb.setValue(100)
    pb.deleteLater()
    iface.messageBar().popWidget(message_widget)
    QgsApplication.processEvents()  # Ensure GUI updates


def create_msg_progress_bar(pb: QProgressBar) -> QgsMessageBar:
    bar = iface.messageBar()
    widget = bar.createMessage("AIAMAS", "Loading style suggestions...")
    widget.layout().addWidget(pb)
    bar.pushWidget(widget, Qgis.Info)
    pb.show()
    QgsApplication.processEvents()  # Ensure GUI updates
    return widget


def create_progress_bar():
    progress_bar = QProgressBar()
    progress_bar.setRange(0, 100)
    progress_bar.setValue(0)
    progress_bar.floatValue = 0
    return progress_bar


def up_progress(pb: QProgressBar, step: float):
    pb.floatValue = pb.floatValue + step
    pb.setValue(round(pb.floatValue))
    QgsApplication.processEvents()  # Ensure GUI updates


def normalize_name(layers: list[VectorLayer]):
    if not layers:
        indicate_error("No vector layers found!", Qgis.Critical)
        return
    num_pattern = '[0-9]'

    for layer in layers:
        layer.normalized_name = remove_characters(layer.name)

