from __future__ import annotations

import os
import urllib.parse

import lxml.etree
from qgis.core import Qgis, QgsApplication, QgsTask
from qgis.PyQt.QtGui import QIcon, QStandardItem

from ..compat import QGSTASK_SILENT, USER_ROLE
from .borg import Borg
from .qrave_map_layer import ProjectTreeData, QRaveMapLayer, QRaveTreeTypes
from .settings import CONSTANTS, Settings
from .util import requestFetch

BASEMAPS_XML_PATH = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "BaseMaps.xml")

MESSAGE_CATEGORY = CONSTANTS["logCategory"]
REQUEST_ARGS = "?service=wms&request=GetCapabilities&version=1.0.0"


class QRaveBaseMap:
    def __init__(self, parent: QStandardItem, layer_url: str, tile_type: str, meta: dict[str, str]):
        self.parent = parent
        self.meta = meta
        self.loaded = False
        self.tile_type = tile_type
        self.settings = Settings()
        self.layer_url = layer_url.replace("?", "")
        self.default_wms_format = "image/png"

        self.tm = QgsApplication.taskManager()
        self.reset()

    def reset(self) -> None:
        if self.tile_type == "wms":
            self.parent.removeRows(0, self.parent.rowCount())
            loading_layer = QStandardItem("loading...")
            f = loading_layer.font()
            f.setItalic(True)
            loading_layer.setFont(f)
            loading_layer.setEnabled(False)
            self.parent.appendRow(loading_layer)

    @staticmethod
    def _local_name(tag: str) -> str:
        """Return the local (namespace-stripped) name for an XML tag."""
        return tag.split("}")[-1] if tag else tag

    def _child_by_localname(self, el, names):
        """Return the first direct child whose local-name matches any in names."""
        if el is None:
            return None
        if isinstance(names, str):
            names = (names,)
        for child in el:
            if self._local_name(child.tag) in names:
                return child
        return None

    def _children_by_localname(self, el, name):
        """Yield direct children with the requested local-name."""
        if el is None:
            return
        for child in el:
            if self._local_name(child.tag) == name:
                yield child

    def _extract_srs(self, el, fallback: str | None) -> str:
        """Return the first SRS/CRS defined on this element or inherit the fallback."""
        srs_el = self._child_by_localname(el, ("SRS", "CRS"))
        if srs_el is not None and srs_el.text:
            first = srs_el.text.split()
            if first:
                return first[0]
        return fallback or "unknown"

    def _parse_wms_layer(self, root_el, parent: QStandardItem, inherited_srs: str | None = None):
        try:
            title_el = self._child_by_localname(root_el, "Title")
            title = title_el.text.strip() if title_el is not None and title_el.text else "Untitled Layer"

            name_el = self._child_by_localname(root_el, "Name")
            name = name_el.text.strip() if name_el is not None and name_el.text else title

            srs = self._extract_srs(root_el, inherited_srs)

            format_el = self._child_by_localname(root_el, "Style")
            legend_format_el = None
            if format_el is not None:
                legend_url_el = self._child_by_localname(format_el, "LegendURL")
                if legend_url_el is not None:
                    legend_format_el = self._child_by_localname(legend_url_el, "Format")
            lyr_format = legend_format_el.text.strip() if legend_format_el is not None and legend_format_el.text else self.default_wms_format

            abstract_el = self._child_by_localname(root_el, "Abstract")
            abstract = abstract_el.text.strip() if abstract_el is not None and abstract_el.text else "No abstract provided"

            url_with_params = f"crs={srs}&format={lyr_format}&layers={name}&styles&url={self.layer_url}"

            has_sublayers = any(True for _ in self._children_by_localname(root_el, "Layer"))
            icon_path = ":/plugins/qrave_toolbar/BrowseFolder.png" if has_sublayers else ":/plugins/qrave_toolbar/layers/Raster.png"
            lyr_item = QStandardItem(QIcon(icon_path), title)

            extra_meta = {"srs": srs, "name": name, "lyr_format": lyr_format}
            map_layer = QRaveMapLayer(title, QRaveMapLayer.LayerTypes.WEBTILE, tile_type=self.tile_type, layer_uri=url_with_params, meta=extra_meta)
            lyr_item.setData(ProjectTreeData(QRaveTreeTypes.LEAF, None, map_layer), USER_ROLE)
            lyr_item.setToolTip(wrap_by_word(abstract, 20))

        except AttributeError as e:
            sourceline = getattr(root_el, "sourceline", None)
            self.settings.log(f"Error parsing basemap layer Exception: {e}, Line: {sourceline}, Url: {self.layer_url + REQUEST_ARGS}", Qgis.Warning)
            title_fallback_el = self._child_by_localname(root_el, "Title")
            fallback_title = title_fallback_el.text.strip() if title_fallback_el is not None and title_fallback_el.text else "Unnamed Layer"
            lyr_item = QStandardItem(QIcon(":/plugins/qrave_toolbar/BrowseFolder.png"), fallback_title)
            lyr_item.setData(ProjectTreeData(QRaveTreeTypes.BASEMAP_SUB_FOLDER), USER_ROLE)

        parent.appendRow(lyr_item)

        for sublyr in self._children_by_localname(root_el, "Layer"):
            self._parse_wms_layer(sublyr, lyr_item, srs)

    def _wms_fetch_done(self, exception, result=None):
        """This is called when doSomething is finished.
        Exception is not None if doSomething raises an exception.
        result is the return value of doSomething."""
        if exception is None:
            if result is None:
                self.settings.log("Completed with no exception and no result ", Qgis.Warning)
            else:
                try:
                    self.parent.removeRows(0, self.parent.rowCount())
                    capability_el = self._child_by_localname(result, "Capability")
                    if capability_el is None:
                        raise ValueError("WMS capabilities missing <Capability> section")

                    request_el = self._child_by_localname(capability_el, "Request")
                    getmap_el = self._child_by_localname(request_el, "GetMap") if request_el is not None else None
                    candidate_formats = []
                    if getmap_el is not None:
                        for fmt_el in self._children_by_localname(getmap_el, "Format"):
                            if fmt_el.text:
                                text = fmt_el.text.strip()
                                if text:
                                    candidate_formats.append(text)
                    for fmt in candidate_formats:
                        if "/" in fmt:  # basic MIME check
                            self.default_wms_format = fmt
                            break
                    else:
                        self.default_wms_format = "image/png"

                    for lyr in self._children_by_localname(capability_el, "Layer"):
                        self._parse_wms_layer(lyr, self.parent)

                except Exception as e:
                    self.settings.log(str(e), Qgis.Critical)

        else:
            self.settings.log(f"Exception: {exception}", Qgis.Critical)
            # Do NOT re-raise inside a QgsTask finished callback — it would
            # propagate into QGIS's task manager and produce unhandled noise.

    def load_layers(self, force: bool = False) -> None:
        if self.loaded and not force:
            return

        def _wms_fetch(task):
            result = requestFetch(self.layer_url + REQUEST_ARGS)
            if isinstance(result, bytes):
                payload = result
            else:
                payload = result.encode("utf-8", errors="ignore")
            try:
                return lxml.etree.fromstring(payload)
            except lxml.etree.XMLSyntaxError as exc:
                preview = payload.decode("utf-8", errors="ignore")[:400]
                raise ValueError(f"WMS capabilities from {self.layer_url} are not valid XML: {exc}. Preview: {preview}")

        if self.tile_type == "wms":
            self.settings.log(f"Fetching WMS Capabilities: {self.layer_url}", Qgis.Info)
            ns_task = QgsTask.fromFunction("Loading WMS Data", _wms_fetch, on_finished=self._wms_fetch_done)
            if QGSTASK_SILENT and hasattr(ns_task, "setFlags"):
                ns_task.setFlags(ns_task.flags() | QGSTASK_SILENT)
            self.tm.addTask(ns_task)


class BaseMaps(Borg):
    def __init__(self):
        Borg.__init__(self)
        if "regions" not in self.__dict__:
            self.regions = {}

    def load(self) -> None:
        self.regions = {}

        # Maybe the basemaps file isn't synced yet
        if not os.path.isfile(BASEMAPS_XML_PATH):
            return

        # Parse the XML
        try:
            for region in lxml.etree.parse(BASEMAPS_XML_PATH).getroot().findall("Region"):
                q_region = QStandardItem(QIcon(":/plugins/qrave_toolbar/layers/basemap.svg"), "Basemaps")
                (q_region.setData(ProjectTreeData(QRaveTreeTypes.BASEMAP_ROOT), USER_ROLE),)
                self.regions[region.attrib["name"]] = q_region

                for group_layer in region.findall("GroupLayer"):
                    q_group_layer = QStandardItem(QIcon(":/plugins/qrave_toolbar/BrowseFolder.png"), group_layer.attrib["name"])
                    (q_group_layer.setData(ProjectTreeData(QRaveTreeTypes.BASEMAP_SUPER_FOLDER), USER_ROLE),)
                    q_region.appendRow(q_group_layer)

                    for layer in group_layer.findall("Layer"):
                        layer_label = layer.attrib["name"]
                        # TODO: Need to go a little backward compatible. We can remove this logic after July 1, 2021
                        tile_type = layer.attrib["type"] if "type" in layer.attrib else "wms"
                        layer_url = layer.attrib["url"]

                        icon = "BrowseFolder.png" if tile_type == "wms" else "layers/satellite.svg"
                        q_layer = QStandardItem(QIcon(f":/plugins/qrave_toolbar/{icon}"), layer_label)

                        meta = {meta.attrib["name"]: meta.text for meta in layer.findall("Metadata/Meta")}

                        basemap_obj = QRaveBaseMap(q_layer, layer_url, tile_type, meta)

                        if tile_type == "wms":
                            pt_data = basemap_obj
                        else:
                            encoded_url = urllib.parse.quote_plus(layer_url)
                            url_with_params = f"type=xyz&url={encoded_url}"
                            pt_data = QRaveMapLayer(layer_label, QRaveMapLayer.LayerTypes.WEBTILE, tile_type=tile_type, layer_uri=url_with_params, meta=meta)

                        # WMS is complicated because it needs a lookup
                        q_layer.setData(ProjectTreeData(QRaveTreeTypes.LEAF, None, pt_data), USER_ROLE)

                        # We set the data to be Basemaps to help us load this stuff later
                        q_group_layer.appendRow(q_layer)
        except Exception as e:
            settings = Settings()
            settings.msg_bar("Error loading basemaps", f"Exception: {e}", Qgis.Critical)


def wrap_by_word(s: str, n: int) -> str:
    """returns a string where \\n is inserted between every n words"""
    a = s.split()
    ret = ""
    for i in range(0, len(a), n):
        ret += " ".join(a[i : i + n]) + "\n"

    return ret
