import traceback

from qgis.core import QgsMessageLog
from qgis.core import Qgis

from geogst.core.geometries.shapes.lines import *
from geogst.core.geometries.shapes.multilines import *
from geogst.core.utils.inspections.errors import *
from geogst.core.utils.inspections.functions import *

from .messages import error_qgis
from .vectors import *


def extract_line_3d_from_qgslinestring_3d(
    linestring: QgsLineString
) -> Ln:

    coords = []

    for pt in linestring:
        coords.append([pt.x, pt.y, pt.z])

    return Ln(coords)


def extract_multiline_3d_from_qgsmultilinestring_3d(
    multilinestring: QgsMultiLineString
) -> MultiLine:

    lines = []

    for line in multilinestring:

        lines.append(
            extract_line_3d_from_qgslinestring_3d(line)
        )

    return MultiLine(lines)


def extract_coords_2d_from_qgslinestring(
    qgslinestring,
) -> List[Tuple[numbers.Real, numbers.Real]]:

    return [(qgspoint.x(), qgspoint.y()) for qgspoint in qgslinestring]


def extract_coords_3d_from_linez_geometry(
    linez_geometry: 'qgs.Geometry',
) -> List[Tuple[numbers.Real, numbers.Real, numbers.Real]]:

    if linez_geometry.isEmpty() or QgsWkbTypes.hasZ(linez_geometry.wkbType()) == False:
        QgsMessageLog.logMessage(
            "Geometry does not contain Z coordinates or is not valid",
            "qgSurf",
            Qgis.MessageLevel.Warning
        )
        return None

    coords_3d = []
    points = linez_geometry.constGet().points()
    for point in points:
        coords_3d.append((point.x(), point.y(), point.z()))

    return coords_3d


def extract_coords_3d_from_multilinez_geometry(
    multilinez_geometry: 'qgs.Geometry',
) -> List[List[Tuple[numbers.Real, numbers.Real, numbers.Real]]]:

    multiline_coords_3d = []
    for linez_geometry in multilinez_geometry:
        line_coords_3d = extract_coords_3d_from_linez_geometry(linez_geometry)
        if line_coords_3d is not None:
            multiline_coords_3d.append(line_coords_3d)

    return multiline_coords_3d


def extract_coords_2d_from_qgsmultilinestring(
    qgsmultilinestring,
) -> List[List[Tuple[numbers.Real, numbers.Real]]]:

    return [extract_coords_2d_from_qgslinestring(qgslinestring) for qgslinestring in qgsmultilinestring]


def extract_coords_2d_with_order_from_selected_features_in_layer_multipart(
    line_layer,
    order_field_ndx: Optional[numbers.Integral] = None
) -> Tuple[bool, Union[str, Tuple[List, List]]]:

    try:

        lines = []
        order_values = []

        if line_layer.selectedFeatureCount() > 0:
            features = line_layer.selectedFeatures()
        else:
            features = line_layer.getFeatures()

        dummy_progressive = 0

        for feature in features:

            dummy_progressive += 1

            order_val = feature[order_field_ndx] if order_field_ndx is not None else dummy_progressive

            geom = feature.geometry()

            # features with no geometry at all, or with an empty one, would
            # otherwise make the conversion to polyline raise and so discard
            # the whole layer

            if geom is None or geom.isNull() or geom.isEmpty():

                QgsMessageLog.logMessage(
                    f"Feature {feature.id()} has null or empty geometry, skipping",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue

            if geom.isMultipart():

                # parts too short to define a line are dropped, as are the
                # features left without any usable part

                coords = [
                    part for part in extract_coords_2d_from_qgsmultilinestring(geom.asMultiPolyline())
                    if len(part) > 1
                ]

                if not coords:

                    QgsMessageLog.logMessage(
                        f"Feature {feature.id()} has no multiline part with at least two points, skipping",
                        "qgSurf",
                        Qgis.MessageLevel.Warning
                    )
                    continue

                lines.append(
                    (
                        'multiline',
                        coords  # geom was QVector<QgsPolyline>
                     )
                )
                # now it's a list of list of (x,y) tuples

            else:

                coords = extract_coords_2d_from_qgslinestring(geom.asPolyline())  # geom is QVector<QgsPointXY>

                if len(coords) < 2:

                    QgsMessageLog.logMessage(
                        f"Feature {feature.id()} has less than two points, skipping",
                        "qgSurf",
                        Qgis.MessageLevel.Warning
                    )
                    continue

                lines.append(
                    (
                        'line',
                        coords
                    )
                )

            order_values.append(order_val)

        if not lines:
            return False, "No line feature with a usable geometry in the source layer"

        return True, (lines, order_values)

    except Exception as e:

        return False, f"{e!r}"


def extract_coords_3d_with_fid_from_selected_features_in_layer_multipart(
    line_layer,
) -> Union[str, List]:

    try:
        # Verify layer is valid
        from qgis.core import QgsVectorLayer, QgsMessageLog
        if not isinstance(line_layer, QgsVectorLayer):
            return f"Invalid layer type: expected QgsVectorLayer, got {type(line_layer)}"
        if not line_layer.isValid():
            return f"Layer is not valid: {line_layer.name() if hasattr(line_layer, 'name') else 'unknown'}"
        
        provider = line_layer.dataProvider()
        if provider is None:
            return f"Layer has no data provider: {line_layer.name() if hasattr(line_layer, 'name') else 'unknown'}"

        lines = []

        if line_layer.selectedFeatureCount() > 0:
            features = line_layer.selectedFeatures()
        else:
            features = line_layer.getFeatures()

        for feature in features:

            fid = feature.id()
            geom = feature.geometry()
            
            # Validate geometry
            if geom is None:
                QgsMessageLog.logMessage(
                    f"Feature {fid} has None geometry, skipping",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue
            
            if geom.isEmpty():
                QgsMessageLog.logMessage(
                    f"Feature {fid} has empty geometry, skipping",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue

            if geom.isMultipart():

                lines.append(
                    (
                        fid,
                        'multiline',
                        extract_coords_3d_from_multilinez_geometry(geom)
                     )
                )
                # now it's a list of list of (x,y,z) tuples

            else:

                line_coords = extract_coords_3d_from_linez_geometry(geom)
                if line_coords is not None:
                    lines.append(
                        (
                            fid,
                            'line',
                            line_coords
                        )
                    )
                else:
                    QgsMessageLog.logMessage(
                        f"Feature {fid} could not extract 3D coordinates (no Z or invalid geometry), skipping",
                        "qgSurf",
                        Qgis.MessageLevel.Warning
                    )

        return lines

    except Exception as e:

        return f"{e!r}"


def extract_selected_lines_2d_with_attributes_from_layer_multipart(
    line_layer,
    field_list=None
) -> List[List]:

    if field_list is None:
        field_list = []

    lines = []

    # Verify layer is valid
    from qgis.core import QgsVectorLayer
    if not isinstance(line_layer, QgsVectorLayer):
        QgsMessageLog.logMessage(
            f"Invalid layer type: expected QgsVectorLayer, got {type(line_layer)}",
            "qgSurf",
            Qgis.MessageLevel.Warning
        )
        return []

    if not line_layer.isValid():
        QgsMessageLog.logMessage(
            f"Layer '{line_layer.name()}' is not valid",
            "qgSurf",
            Qgis.MessageLevel.Warning
        )
        return []

    # Verify provider is valid
    provider = line_layer.dataProvider()
    if provider is None:
        QgsMessageLog.logMessage(
            f"Layer '{line_layer.name()}' has no valid data provider",
            "qgSurf",
            Qgis.MessageLevel.Warning
        )
        return []

    # Get field indices, filtering out -1 (field not found)
    field_indices = []
    for field_name in field_list:
        idx = provider.fieldNameIndex(field_name)
        if idx >= 0:
            field_indices.append(idx)
        else:
            QgsMessageLog.logMessage(
                f"Field '{field_name}' not found in layer '{line_layer.name()}'",
                "qgSurf",
                Qgis.MessageLevel.Warning
            )

    if line_layer.selectedFeatureCount() > 0:
        features = line_layer.selectedFeatures()
    else:
        features = line_layer.getFeatures()

    for feature in features:

        geom = feature.geometry()

        # null and empty geometries cannot be converted to polylines: they
        # would raise and so discard the whole layer

        if geom is None or geom.isNull() or geom.isEmpty():

            QgsMessageLog.logMessage(
                f"Feature {feature.id()} has null or empty geometry, skipping",
                "qgSurf",
                Qgis.MessageLevel.Warning
            )
            continue

        if geom.isMultipart():
            rec_geom = extract_coords_2d_from_qgsmultilinestring(geom.asMultiPolyline())
        else:
            rec_geom = [extract_coords_2d_from_qgslinestring(geom.asPolyline())]

        rec_geom = [part for part in rec_geom if len(part) > 1]

        if not rec_geom:

            QgsMessageLog.logMessage(
                f"Feature {feature.id()} has no part with at least two points, skipping",
                "qgSurf",
                Qgis.MessageLevel.Warning
            )
            continue

        # Use direct attribute access with index instead of attrs[field_ndx].name()
        rec_data = [str(feature.attribute(field_ndx)) for field_ndx in field_indices]

        lines.append([rec_geom, rec_data])

    return lines


def extract_line_23d_from_qgslinestring_23d(
    line: QgsLineString
) -> Tuple[bool, Union[str, Ln]]:

    try:

        coords = []

        dim = set()

        for point in line.points():

            if np.isfinite(point.z()):
                coords.append([point.x(), point.y(), point.z()])
                dim.add(3)
            else:
                coords.append([point.x(), point.y()])
                dim.add(2)

        # an empty part is not an error, just nothing to read: it is the
        # caller that drops the lines with too few points

        if len(dim) > 1:
            return False, f"Dimension variety is {len(dim)} (should be 1)"

        return True, Ln(coords) if coords else Ln()

    except Exception as e:

        return False, f"{e!r}"


def extract_lines_23d_from_qgslinestring_multipart(
    qgis_geometry,
) -> Union[None, List[Ln]]:
    """
    Extract geometry as lines list and attributes from line layer.
    """

    try:

        lns = []

        if qgis_geometry is None or qgis_geometry.isNull() or qgis_geometry.isEmpty():

            QgsMessageLog.logMessage(
                "Null or empty line geometry, skipping",
                "qgSurf",
                Qgis.MessageLevel.Warning
            )

            return lns

        # the parts, for single part geometries as well: a QgsGeometry has no
        # points() of its own, so reading it directly used to raise and drop
        # every single part line (i.e. every line of a GeoPackage layer)

        for subline in qgis_geometry.parts():

            success, result = extract_line_23d_from_qgslinestring_23d(subline)

            if not success:
                msg = result
                error_qgis(
                    "Line extraction failed",
                    f"{msg}",
                )
                return None

            ln = result

            if ln.num_points() < 2:

                QgsMessageLog.logMessage(
                    "Line part with less than two points, skipping",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue

            lns.append(ln)

        return lns

    except Exception as e:

        err = Error(
            True,
            caller_name(),
            e,
            traceback.format_exc()
        )

        error_qgis(
            "Line extraction failed",
            f"{err!r}",
        )

        return None

