from qgis.core import (
    QgsWkbTypes,
    QgsReadWriteContext,
    QgsProject,
    QgsVectorFileWriter,
    QgsCoordinateTransform,
    QgsPointXY,
    QgsField,
    QgsVectorLayer,
    # QgsCoordinateReferenceSystem,
    QgsFeature,
    QgsPoint,
    # QgsGeometry,
    QgsLayerTree,
    QgsLayerTreeNode,
)
from qgis.PyQt.QtCore import QVariant
from .type_mapping import type_pyd_to_Qvar
from typing import Dict, List
from copy import copy
import shutil
import os

import errno
from pathlib import Path
from osgeo import ogr
from qgis.PyQt.QtXml import QDomDocument

# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer


def load_icons():
    """
    Copy the plugin SVG icons (including ki-water-icons) into the user's profile
    without touching icons installed by other plugins.

    We keep a manifest of what we copied so that clean_icons() can remove only
    our files.
    """

    src_dir = Path(__file__).parent / "icons"
    target_dir = (Path(__file__).parent / "../../../svg/icons").resolve()
    manifest = target_dir / "network-store-icons.txt"

    def _manifested_files() -> list[str]:
        if not manifest.exists():
            return []
        return [
            line.strip()
            for line in manifest.read_text(encoding="utf-8").splitlines()
            if line.strip()
        ]

    # Remove previously copied icons listed in the manifest
    if manifest.exists():
        for name in _manifested_files():
            candidate = target_dir / Path(name)
            try:
                if candidate.exists() and candidate.is_file():
                    candidate.unlink()
                    # Clean up empty parent folders (e.g., ki-water-icons) but keep target_dir
                    parent = candidate.parent
                    while parent != target_dir:
                        try:
                            parent.rmdir()
                        except OSError:
                            break
                        parent = parent.parent
            except Exception:
                # Ignore removal issues; we don't want to break plugin load
                pass
        try:
            manifest.unlink()
        except Exception:
            pass

    target_dir.mkdir(parents=True, exist_ok=True)

    # Collect SVG sources from the root icons dir and ki-water-icons subdir
    sources: list[Path] = sorted(src_dir.rglob("*.svg"))

    copied: list[str] = []
    for src in sources:
        try:
            rel = src.relative_to(src_dir)
            dest = target_dir / rel
            dest.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(src, dest)
            copied.append(rel.as_posix())
        except OSError as e:
            # If a copy fails, keep going so other icons still load
            if e.errno != errno.ENOTDIR:
                continue

    if copied:
        try:
            manifest.write_text("\n".join(copied), encoding="utf-8")
        except Exception:
            pass


def clean_icons():
    """
    Remove only the icons that this plugin installed (tracked via manifest),
    leaving any other plugin's icons intact.
    """
    target_dir = (Path(__file__).parent / "../../../svg/icons").resolve()
    manifest = target_dir / "network-store-icons.txt"
    if not target_dir.exists() or not manifest.exists():
        return

    try:
        for line in manifest.read_text(encoding="utf-8").splitlines():
            name = line.strip()
            if not name:
                continue
            candidate = target_dir / Path(name)
            try:
                if candidate.exists() and candidate.is_file():
                    candidate.unlink()
                    parent = candidate.parent
                    while parent != target_dir:
                        try:
                            parent.rmdir()
                        except OSError:
                            break
                        parent = parent.parent
            except Exception:
                # Ignore individual removal errors to avoid breaking unload
                pass
        manifest.unlink()
    except Exception:
        # Best-effort cleanup; ignore errors
        pass


def new_layer(
    geom_type="Point", layer_name: str = "unnamed_layer", provider: str = "ogr"
):
    layer = QgsVectorLayer(geom_type, layer_name, provider)
    print(layer.isValid())
    return layer


def add_gpkg_layer(
    iface, gpkg, layer
):  # adds EXISTING layer to the project from a gpkg
    layers = [layer.GetName() for layer in ogr.Open(gpkg)]
    if layer in layers:
        return QgsProject.instance().addMapLayer(
            QgsVectorLayer(gpkg + "|layername=" + layer, layer, "ogr"), False
        )
        # return iface.addVectorLayer(gpkg + "|layername=" + layer, layer, 'ogr')
    else:
        print('Error: there is no layer named "{}" in {}!'.format(layer, gpkg))


def add_fields_from_schema(layer: QgsVectorLayer, schema):
    _schema = copy(schema)

    fields = list(_schema["properties"].keys())
    not_null_fields = _schema["required"]
    optional_fields = list(set(fields) - set(not_null_fields))

    for f in not_null_fields:
        p = _schema["properties"][f]
        if "type" in p:
            if p["type"] in type_pyd_to_Qvar:
                layer.dataProvider().addAttributes(
                    [
                        QgsField(
                            name=f,
                            type=type_pyd_to_Qvar[p["type"]],
                            comment=p["description"] if "description" in p else "",
                        )
                    ]
                )
            # elif p['type'] == 'object':
            #     #TODO import the object too with a p['title']. prefix
            #     continue
            else:
                raise TypeError(
                    f"Unsupported type: {p['type']} see type_mapping.py for reference"
                )
        if (
            "allOf" in p
        ):  # Union of values. Types are static here, use it as a string and parse it later
            layer.dataProvider().addAttributes(
                [
                    QgsField(
                        name=f,
                        type=QVariant.String,
                        comment=p["description"] if "description" in p else "",
                    )
                ]
            )
        layer.updateFields()

    for f in optional_fields:
        p = _schema["properties"][f]
        if "type" in p:
            if p["type"] in type_pyd_to_Qvar:
                layer.dataProvider().addAttributes(
                    [
                        QgsField(
                            name=f,
                            type=type_pyd_to_Qvar[p["type"]],
                            comment=p["description"] if "description" in p else "",
                        )
                    ]
                )
            elif p["type"] == "object":
                layer.dataProvider().addAttributes(
                    [
                        QgsField(
                            name=f,
                            type=type_pyd_to_Qvar[p["type"]],
                            comment=p["description"] if "description" in p else "",
                        )
                    ]
                )
                continue
            else:
                raise TypeError(
                    f"Unsupported type: {p['type']} see type_mapping.py for reference"
                )
        if (
            "allOf" in p
        ):  # Union of values. Types are static here, use it as a string and parse it later
            layer.dataProvider().addAttributes(
                [
                    QgsField(
                        name=f,
                        type=QVariant.String,
                        comment=p["description"] if "description" in p else "",
                    )
                ]
            )
        layer.updateFields()


def generate_attribute_form(layer: QgsVectorLayer, schema):
    pass


def apply_style_from_qml(layer: QgsVectorLayer, style_path: str):
    if Path(style_path).suffix == ".qml":
        layer.loadNamedStyle(style_path)
    else:
        raise OSError(f"Invalid extension: {Path(style_path).suffix} it must be .QML")


def saving_gpkg(styled_layer, out_path):
    context = QgsProject.instance().transformContext()
    name = styled_layer.name()
    options = QgsVectorFileWriter.SaveVectorOptions()
    options.layerName = name
    if os.path.exists(out_path):
        options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer
    else:
        options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteFile
    options.fileEncoding = styled_layer.dataProvider().encoding()
    options.driverName = "GPKG"
    QgsVectorFileWriter.writeAsVectorFormatV2(styled_layer, out_path, context, options)
    doc = QDomDocument()
    context = QgsReadWriteContext()
    styled_layer.exportNamedStyle(doc)
    gpkg_layer = QgsVectorLayer(f"{out_path}|layername={name}", name, "ogr")
    gpkg_layer.importNamedStyle(doc)
    gpkg_layer.saveStyleToDatabase(name, "", True, "")


def save_with_description(layer, outputfile, type=QgsWkbTypes.PointZ):
    options = QgsVectorFileWriter.SaveVectorOptions()
    # options.overrideGeometryType = type

    # If geopackage exists, append layer to it, else create it
    if os.path.exists(outputfile):
        options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer
    else:
        options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteFile
    # Use input layer abstract and name in the geopackage
    options.layerOptions = [f"DESCRIPTION={layer.abstract()}"]
    options.layerName = layer.name()

    error, error_string = QgsVectorFileWriter.writeAsVectorFormat(
        layer, outputfile, options
    )
    if error != QgsVectorFileWriter.NoError:
        print("Oh an error has happened: {details}".format(details=error_string))

    return (error, error_string)


def append_geometry(
    element: Dict,
    feature: QgsFeature,
    geomtype: QgsWkbTypes,
    tr: QgsCoordinateTransform,
) -> Dict:
    # add geometry
    geom = feature.geometry()
    if geomtype == QgsWkbTypes.PointGeometry:
        print("")
        p = tr.transform(geom.asPoint())
        if isinstance(p, QgsPointXY):
            element["location"] = {"x": p.x(), "y": p.y()}
        elif isinstance(p, QgsPoint):
            element["location"] = {"x": p.x(), "y": p.y(), "z": p.z()}
        else:
            raise TypeError(
                f"geometry class {type(p).__name__} not supported, only QgsPoint and QgsPointXY"
            )
        # Point feature -> location:Location
    elif geomtype == QgsWkbTypes.LineGeometry:
        # Line feature -> vertices:[Location]\
        vertices = []
        polyline = geom.asPolyline()
        for p in polyline:
            p = tr.transform(p)
            if isinstance(p, QgsPointXY):
                vertices.append({"x": p.x(), "y": p.y()})
            elif isinstance(p, QgsPoint):
                vertices.append({"x": p.x(), "y": p.y(), "z": p.z()})
            else:
                raise TypeError(
                    f"geometry class {type(p).__name__} not supported, only QgsPoint and QgsPointXY"
                )
        element["vertices"] = vertices
    else:
        raise TypeError(
            f"Type: {geomtype} is not supported, only Point and Line geometries"
        )


def get_network_groups(root: QgsLayerTree) -> List:
    result = []
    for node in root.children():  # iterate through the child nodes of the root node
        if (
            node.nodeType() == QgsLayerTreeNode.NodeType.NodeGroup
        ):  # check if the node is a layer group
            subgroup_names = [
                c.name()
                for c in node.children()
                if c.nodeType() == QgsLayerTreeNode.NodeType.NodeGroup
            ]
            if set(["Links", "Nodes"]).issubset(set(subgroup_names)):
                result.append(node.name())
    return result
