from qgis.core import QgsCoordinateReferenceSystem, QgsGeometry, QgsWkbTypes

TARGET_EPSG = "EPSG:6316"
TARGET_CRS = QgsCoordinateReferenceSystem(TARGET_EPSG)

PARCEL_FIELDS = [
    "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_FIELDS = [
    "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 _field_names(layer):
    return [f.name() for f in layer.fields()]


def _safe_text(value):
    if value is None:
        return ""
    return str(value).strip()


def _norm_text(value):
    return _safe_text(value).upper()


def _parse_area_value(value):
    txt = _safe_text(value)
    if txt == "":
        return None

    txt = txt.replace(",", ".")
    try:
        return round(float(txt), 2)
    except Exception:
        return None


def _calc_geom_area(geom):
    if geom is None or geom.isEmpty():
        return None
    try:
        return round(float(geom.area()), 2)
    except Exception:
        return None


def _intersection_area(geom1, geom2):
    if geom1 is None or geom2 is None or geom1.isEmpty() or geom2.isEmpty():
        return 0.0
    try:
        inter = geom1.intersection(geom2)
        if inter.isEmpty():
            return 0.0
        return float(inter.area())
    except Exception:
        return 0.0


def _err(layer_name, fid, message):
    return {
        "layer_name": layer_name,
        "feature_id": fid,
        "message": message,
    }


def _warn(layer_name, fid, message):
    return {
        "layer_name": layer_name,
        "feature_id": fid,
        "message": message,
    }


def _check_required_fields(layer, required_fields):
    names = _field_names(layer)
    missing = [f for f in required_fields if f not in names]
    return missing


def _check_crs(layer):
    crs = layer.crs()
    if not crs.isValid():
        return False, "Layer has no valid CRS"
    if crs.authid() != TARGET_EPSG:
        return False, f"Layer CRS is {crs.authid()}, expected {TARGET_EPSG}"
    return True, None


def _check_geometry_type(layer):
    wkb = layer.wkbType()
    if QgsWkbTypes.geometryType(wkb) != QgsWkbTypes.PolygonGeometry:
        return False, f"Layer geometry is not polygon: {QgsWkbTypes.displayString(wkb)}"
    return True, None


def _check_no_overlaps(layer):
    errors = []
    features = list(layer.getFeatures())
    layer_name = layer.name()

    for i in range(len(features)):
        f1 = features[i]
        g1 = f1.geometry()
        if g1 is None or g1.isEmpty():
            continue

        for j in range(i + 1, len(features)):
            f2 = features[j]
            g2 = f2.geometry()
            if g2 is None or g2.isEmpty():
                continue

            area = _intersection_area(g1, g2)
            if area > 0.000001:
                msg = f"Overlap found between features {f1.id()} and {f2.id()} (area={area:.4f})"
                errors.append(_err(layer_name, f1.id(), msg))
                errors.append(_err(layer_name, f2.id(), msg))

    return errors


def _validate_features(layer, critical_attrs):
    errors = []
    warnings = []
    layer_name = layer.name()

    for feat in layer.getFeatures():
        fid = feat.id()
        geom = feat.geometry()

        if geom is None:
            errors.append(_err(layer_name, fid, "Geometry is NULL"))
            continue

        if geom.isEmpty():
            errors.append(_err(layer_name, fid, "Geometry is empty"))
            continue

        if not geom.isGeosValid():
            errors.append(_err(layer_name, fid, "Invalid geometry"))

        if QgsWkbTypes.geometryType(geom.wkbType()) != QgsWkbTypes.PolygonGeometry:
            errors.append(
                _err(
                    layer_name,
                    fid,
                    f"Geometry is not polygon ({QgsWkbTypes.displayString(geom.wkbType())})"
                )
            )
            continue

        for attr_name in critical_attrs:
            value = feat[attr_name]
            if value is None or str(value).strip() == "":
                warnings.append(_warn(layer_name, fid, f"Empty attribute '{attr_name}'"))

        if "AREA" in feat.fields().names():
            area_attr = _parse_area_value(feat["AREA"])
            area_geom = _calc_geom_area(geom)

            if area_attr is None:
                errors.append(_err(layer_name, fid, "AREA is empty or invalid"))
            elif area_geom is None:
                errors.append(_err(layer_name, fid, "Geometry area could not be calculated"))
            elif area_attr != area_geom:
                errors.append(
                    _err(
                        layer_name,
                        fid,
                        f"AREA ({area_attr:.2f}) does not match geometry area ({area_geom:.2f})"
                    )
                )

    return errors, warnings


def _check_unique_parcno(layer):
    errors = []
    seen = {}
    layer_name = layer.name()

    for feat in layer.getFeatures():
        fid = feat.id()
        raw_parcno = feat["PARCNO"]
        parcno = _norm_text(raw_parcno)

        if parcno == "":
            errors.append(_err(layer_name, fid, "PARCNO is empty"))
            continue

        if parcno in seen:
            msg = f"Duplicate PARCNO '{_safe_text(raw_parcno)}' found in features {seen[parcno]} and {fid}"
            errors.append(_err(layer_name, seen[parcno], msg))
            errors.append(_err(layer_name, fid, msg))
        else:
            seen[parcno] = fid

    return errors


def _check_unique_building_key(layer):
    errors = []
    seen = {}
    layer_name = layer.name()

    for feat in layer.getFeatures():
        fid = feat.id()

        broj_raw = feat["BROJ"]
        del_raw = feat["DEL"]
        bzg_raw = feat["BZG"]

        broj = _norm_text(broj_raw)
        del_ = _norm_text(del_raw)
        bzg = _norm_text(bzg_raw)

        if broj == "" or del_ == "" or bzg == "":
            errors.append(_err(layer_name, fid, "BROJ, DEL and BZG must all be filled"))
            continue

        key = (broj, del_, bzg)

        if key in seen:
            msg = (
                f"Duplicate building key BROJ='{_safe_text(broj_raw)}', "
                f"DEL='{_safe_text(del_raw)}', BZG='{_safe_text(bzg_raw)}' "
                f"found in features {seen[key]} and {fid}"
            )
            errors.append(_err(layer_name, seen[key], msg))
            errors.append(_err(layer_name, fid, msg))
        else:
            seen[key] = fid

    return errors


def _count_containing_parcels(building_geom, parcels_layer):
    count = 0
    containing_ids = []

    for parcel in parcels_layer.getFeatures():
        pgeom = parcel.geometry()
        if pgeom is None or pgeom.isEmpty():
            continue

        try:
            if building_geom.within(pgeom) or building_geom.coveredBy(pgeom):
                count += 1
                containing_ids.append(parcel.id())
        except Exception:
            continue

    return count, containing_ids


def _check_buildings_within_single_parcel(buildings_layer, parcels_layer):
    errors = []
    layer_name = buildings_layer.name()

    if buildings_layer is None or parcels_layer is None:
        errors.append(_err(layer_name, -1, "Buildings/parcels layer missing for containment check"))
        return errors

    for feat in buildings_layer.getFeatures():
        fid = feat.id()
        geom = feat.geometry()

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

        count, containing_ids = _count_containing_parcels(geom, parcels_layer)

        if count == 0:
            errors.append(_err(layer_name, fid, "Building is not fully inside any parcel"))
        elif count > 1:
            errors.append(
                _err(
                    layer_name,
                    fid,
                    f"Building is fully inside multiple parcels {containing_ids}, which is invalid"
                )
            )

    return errors


def _check_parcel_gaps(layer):
    errors = []
    layer_name = layer.name()

    geoms = []
    for feat in layer.getFeatures():
        geom = feat.geometry()
        if geom is None or geom.isEmpty():
            continue
        geoms.append(geom)

    if not geoms:
        return errors

    try:
        union_geom = QgsGeometry.unaryUnion(geoms)
    except Exception:
        errors.append(_err(layer_name, -1, "Could not compute parcel union for gap check"))
        return errors

    if union_geom is None or union_geom.isEmpty():
        return errors

    try:
        if QgsWkbTypes.isMultiType(union_geom.wkbType()):
            multi = union_geom.asMultiPolygon()
            hole_count = 0
            for poly in multi:
                if not poly:
                    continue
                if len(poly) > 1:
                    hole_count += len(poly) - 1

            if hole_count > 0:
                errors.append(
                    _err(
                        layer_name,
                        -1,
                        f"Parcel coverage has {hole_count} internal gap(s) / hole(s)"
                    )
                )
        else:
            poly = union_geom.asPolygon()
            if poly and len(poly) > 1:
                hole_count = len(poly) - 1
                errors.append(
                    _err(
                        layer_name,
                        -1,
                        f"Parcel coverage has {hole_count} internal gap(s) / hole(s)"
                    )
                )
    except Exception:
        errors.append(_err(layer_name, -1, "Could not inspect parcel gaps"))
        return errors

    return errors


def validate_layer(layer, required_fields, critical_attrs, layer_kind):
    report = {
        "layer_name": layer.name(),
        "ok": True,
        "errors": [],
        "warnings": [],
        "feature_count": layer.featureCount(),
    }

    if layer is None or not layer.isValid():
        report["ok"] = False
        report["errors"].append(_err("UNKNOWN", -1, "Layer is missing or invalid"))
        return report

    missing = _check_required_fields(layer, required_fields)
    if missing:
        report["ok"] = False
        report["errors"].append(
            _err(layer.name(), -1, "Missing fields: " + ", ".join(missing))
        )

    ok_crs, crs_msg = _check_crs(layer)
    if not ok_crs:
        report["ok"] = False
        report["errors"].append(_err(layer.name(), -1, crs_msg))

    ok_geom_type, geom_msg = _check_geometry_type(layer)
    if not ok_geom_type:
        report["ok"] = False
        report["errors"].append(_err(layer.name(), -1, geom_msg))

    if layer.featureCount() <= 0:
        report["ok"] = False
        report["errors"].append(_err(layer.name(), -1, "Layer has no features"))

    f_errors, f_warnings = _validate_features(layer, critical_attrs)
    if f_errors:
        report["ok"] = False
        report["errors"].extend(f_errors)

    if f_warnings:
        report["warnings"].extend(f_warnings)

    overlap_errors = _check_no_overlaps(layer)
    if overlap_errors:
        report["ok"] = False
        report["errors"].extend(overlap_errors)

    if layer_kind == "parcel":
        unique_errors = _check_unique_parcno(layer)
        if unique_errors:
            report["ok"] = False
            report["errors"].extend(unique_errors)

        gap_errors = _check_parcel_gaps(layer)
        if gap_errors:
            report["ok"] = False
            report["errors"].extend(gap_errors)

    elif layer_kind == "building":
        unique_errors = _check_unique_building_key(layer)
        if unique_errors:
            report["ok"] = False
            report["errors"].extend(unique_errors)

    return report


def validate_project(parcels_layer, buildings_layer):
    parcels_report = validate_layer(
        parcels_layer,
        PARCEL_FIELDS,
        critical_attrs=["PARCNO", "CADMUN", "AREA"],
        layer_kind="parcel"
    )

    buildings_report = validate_layer(
        buildings_layer,
        BUILDING_FIELDS,
        critical_attrs=["PARCNO", "AREA"],
        layer_kind="building"
    )

    containment_errors = _check_buildings_within_single_parcel(
        buildings_layer,
        parcels_layer
    )
    if containment_errors:
        buildings_report["ok"] = False
        buildings_report["errors"].extend(containment_errors)

    ok = parcels_report["ok"] and buildings_report["ok"]

    return {
        "ok": ok,
        "parcels": parcels_report,
        "buildings": buildings_report,
    }


def format_validation_report(result):
    lines = []

    overall = "READY FOR EXPORT" if result["ok"] else "EXPORT BLOCKED"
    lines.append(f"Status: {overall}")
    lines.append("")

    for section_name, report in [
        ("PARC_TRANS", result["parcels"]),
        ("BUILDINGS_TRANS", result["buildings"])
    ]:
        lines.append(f"{section_name}")
        lines.append(f"  Features: {report['feature_count']}")
        lines.append(f"  OK: {'YES' if report['ok'] else 'NO'}")

        if report["errors"]:
            lines.append("  Errors:")
            for e in report["errors"]:
                lines.append(f"    - {e['message']}")

        if report["warnings"]:
            lines.append("  Warnings:")
            for w in report["warnings"]:
                lines.append(f"    - {w['message']}")

        lines.append("")

    return "\n".join(lines)