import os
import shutil
import tempfile
import zipfile
from xml.etree import ElementTree as ET

from qgis.core import QgsWkbTypes


WFS_NS = "http://www.opengis.net/wfs"
GML_NS = "http://www.opengis.net/gml"
AKN_NS = "http://www.openplans.org/topp"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"

ET.register_namespace("wfs", WFS_NS)
ET.register_namespace("gml", GML_NS)
ET.register_namespace("akn", AKN_NS)
ET.register_namespace("xsi", XSI_NS)


PARCEL_FIELD_ORDER = [
    "ID",
    "ID_PROC",
    "ID_PROC_EXPORT",
    "ID_PARC",
    "ID_PP",
    "MAIN_PARCEL",
    "OPS_KOPS",
    "PLIST",
    "BROJ",
    "DEL",
    "BZG",
    "KLASA",
    "SKICA",
    "SOPS",
    "AREA",
    "PARCNO",
    "CADMUN",
    "GIS_ELAB",
    "GIS_WORK",
    "KULT",
    "MESTO",
    "LOCK_ID_PROC",
]

BUILDING_FIELD_ORDER = [
    "ID",
    "ID_PROC",
    "ID_PROC_EXPORT",
    "ID_PARC",
    "ID_PP",
    "MAIN_PARCEL",
    "GIS_WORK",
    "CADMUN",
    "BUILDNO",
    "PARCNO",
    "OPS",
    "PLIST",
    "BROJ",
    "DEL",
    "BZG",
    "KLASA",
    "SKICA",
    "SOPS",
    "AREA",
    "GIS_ELAB",
    "KULT",
    "MESTO",
    "TYPE",
    "ADDRESS",
    "OWNER",
    "LOCK_ID_PROC",
]


def _pretty_indent(elem, level=0):
    i = "\n" + level * "  "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
        for e in elem:
            _pretty_indent(e, level + 1)
        if not e.tail or not e.tail.strip():
            e.tail = i
    if level and (not elem.tail or not elem.tail.strip()):
        elem.tail = i


def _coords_from_polygon_geometry(geom):
    if geom is None or geom.isEmpty():
        return None

    if QgsWkbTypes.geometryType(geom.wkbType()) != QgsWkbTypes.PolygonGeometry:
        return None

    if QgsWkbTypes.isMultiType(geom.wkbType()):
        multi = geom.asMultiPolygon()
        if not multi or len(multi) != 1:
            return None
        poly = multi[0]
    else:
        poly = geom.asPolygon()

    if not poly:
        return None

    outer = poly[0]
    holes = poly[1:] if len(poly) > 1 else []

    def ring_to_text(ring):
        return " ".join([f"{pt.x()},{pt.y()}" for pt in ring])

    return {
        "outer": ring_to_text(outer),
        "holes": [ring_to_text(h) for h in holes],
    }


def _add_polygon_geometry(parent_elem, geom):
    coords = _coords_from_polygon_geometry(geom)
    if coords is None:
        return

    geom_elem = ET.SubElement(parent_elem, f"{{{AKN_NS}}}GEOMETRY")
    poly_elem = ET.SubElement(geom_elem, f"{{{GML_NS}}}Polygon")

    outer_elem = ET.SubElement(poly_elem, f"{{{GML_NS}}}outerBoundaryIs")
    outer_ring = ET.SubElement(outer_elem, f"{{{GML_NS}}}LinearRing")
    outer_coords = ET.SubElement(outer_ring, f"{{{GML_NS}}}coordinates")
    outer_coords.text = coords["outer"]

    for hole in coords["holes"]:
        inner_elem = ET.SubElement(poly_elem, f"{{{GML_NS}}}innerBoundaryIs")
        inner_ring = ET.SubElement(inner_elem, f"{{{GML_NS}}}LinearRing")
        inner_coords = ET.SubElement(inner_ring, f"{{{GML_NS}}}coordinates")
        inner_coords.text = hole


def _feature_sort_key(feature, primary_field, secondary_field="ID"):
    v1 = feature[primary_field] if primary_field in feature.fields().names() else ""
    v2 = feature[secondary_field] if secondary_field in feature.fields().names() else ""
    return (str(v1 or ""), str(v2 or ""), feature.id())


def _build_root(schema_location):
    root = ET.Element(
        f"{{{WFS_NS}}}FeatureCollection",
        {
            f"{{{XSI_NS}}}schemaLocation": schema_location
        }
    )
    return root


def write_parcels_gml(layer, output_path):
    schema_location = (
        "http://www.openplans.org/topp "
        "http://128.128.0.105/MakEditProxy/dzgr_build_trans.xsd "
        "http://www.opengis.net/wfs "
        "http://128.128.0.105/MakEditProxy/WFS-basic.xsd"
    )

    root = _build_root(schema_location)

    features = sorted(
        list(layer.getFeatures()),
        key=lambda f: _feature_sort_key(f, "PARCNO", "ID")
    )

    for idx, feat in enumerate(features, start=1):
        member = ET.SubElement(root, f"{{{GML_NS}}}featureMember")
        parcel_elem = ET.SubElement(
            member,
            f"{{{AKN_NS}}}PARC_TRANS",
            {"fid": f"PARC_TRANS.{idx}"}
        )

        for field_name in PARCEL_FIELD_ORDER:
            child = ET.SubElement(parcel_elem, f"{{{AKN_NS}}}{field_name}")
            value = feat[field_name]
            child.text = "" if value is None else str(value)

        _add_polygon_geometry(parcel_elem, feat.geometry())

    _pretty_indent(root)
    tree = ET.ElementTree(root)
    tree.write(output_path, encoding="utf-8", xml_declaration=True)


def write_buildings_gml(layer, output_path):
    schema_location = (
        "http://www.openplans.org/topp "
        "http://128.128.0.105/MakEditProxy/dzgr_parc_trans.xsd "
        "http://www.opengis.net/wfs "
        "http://128.128.0.105/MakEditProxy/WFS-basic.xsd"
    )

    root = _build_root(schema_location)

    features = sorted(
        list(layer.getFeatures()),
        key=lambda f: _feature_sort_key(f, "BUILDNO", "ID")
    )

    for idx, feat in enumerate(features, start=1):
        member = ET.SubElement(root, f"{{{GML_NS}}}featureMember")
        building_elem = ET.SubElement(
            member,
            f"{{{AKN_NS}}}BUILDINGS_TRANS",
            {"fid": f"BUILDINGS_TRANS.{idx}"}
        )

        for field_name in BUILDING_FIELD_ORDER:
            child = ET.SubElement(building_elem, f"{{{AKN_NS}}}{field_name}")
            value = feat[field_name]
            child.text = "" if value is None else str(value)

        _add_polygon_geometry(building_elem, feat.geometry())

    _pretty_indent(root)
    tree = ET.ElementTree(root)
    tree.write(output_path, encoding="utf-8", xml_declaration=True)


def export_final_zip(package, session_info, output_zip_path):
    if package is None:
        raise Exception("No source package loaded.")

    if session_info is None:
        raise Exception("No editable layers available.")

    parcels_layer = session_info["parcels_layer"]
    buildings_layer = session_info["buildings_layer"]

    work_dir = tempfile.mkdtemp(prefix="cadastre_export_")

    parcels_gml = os.path.join(work_dir, "parcels.gml")
    buildings_gml = os.path.join(work_dir, "buildings.gml")
    parcels_o_gml = os.path.join(work_dir, "parcels_o.gml")
    buildings_o_gml = os.path.join(work_dir, "buildings_o.gml")
    elab_xml = os.path.join(work_dir, "elab.xml")

    write_parcels_gml(parcels_layer, parcels_gml)
    write_buildings_gml(buildings_layer, buildings_gml)

    shutil.copy2(package.get_path("parcels_o.gml"), parcels_o_gml)
    shutil.copy2(package.get_path("buildings_o.gml"), buildings_o_gml)
    shutil.copy2(package.get_path("elab.xml"), elab_xml)

    with zipfile.ZipFile(output_zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
        zf.write(parcels_o_gml, arcname="parcels_o.gml")
        zf.write(parcels_gml, arcname="parcels.gml")
        zf.write(buildings_o_gml, arcname="buildings_o.gml")
        zf.write(buildings_gml, arcname="buildings.gml")
        zf.write(elab_xml, arcname="elab.xml")

    return output_zip_path