import logging
from dataclasses import dataclass
from enum import Enum
from typing import List
from sqlalchemy import text
from SAGisXPlanung import Session
from SAGisXPlanung.XPlan.feature_types import XP_Plan
from SAGisXPlanung.config import table_name_to_class
logger = logging.getLogger(__name__)
GRID_TOLERANCE = 0.001

class GeometryIntersectionType(Enum):
    """ Gibt an, welcher Grund einen Überschneidungsfehler hevorgerufen hat. """
    Planinhalt = 'Flächenschlussobjekt weist Überschneidung auf'
    Bereich = 'Planinhalt liegt nicht vollständig im Bereich'
    Plan = 'Bereich liegt nicht vollständig im Geltungsbereich des Plans'
    NotCovered = 'Kein Flächenschluss vorliegend'
    FullyWithin = 'Planinhalt wird vollständig überdeckt'

@dataclass
class ValidationResult:
    xid: str
    xtype: type
    error_msg: str = None
    geom_wkt: str = None
    intersection_type: GeometryIntersectionType = None
    other_xid: str = None
    other_xtype: type = None

    def __post_init__(self):
        if self.intersection_type is not None:
            self.error_msg = self.intersection_type.value
        elif self.error_msg is None:
            self.error_msg = 'Fehler in der Geometrievalidierung'

def _validate_overlaps(plan_id, short_plan_type: str) -> List[ValidationResult]:
    """ validates if any of the plan contents overlap each other"""
    result = []
    with Session.begin() as session:
        stmt = text(f"""\n            WITH all_objekt_positions AS (\n                SELECT id, flaechenschluss, position FROM bp_objekt\n                UNION ALL\n                SELECT id, flaechenschluss, position FROM fp_objekt\n                UNION ALL\n                SELECT id, flaechenschluss, position FROM lp_objekt\n                UNION ALL\n                SELECT id, flaechenschluss, position FROM so_objekt\n            )\n            SELECT\n                ST_AsText(polygon_geom) AS wkt,\n                xp_a.id AS a_xid, xp_a.type AS a_type,\n                xp_b.id AS b_xid, xp_b.type AS b_type,\n                st_within(a.position, b.position) as is_within\n            FROM all_objekt_positions a\n                CROSS JOIN all_objekt_positions b\n                INNER JOIN xp_objekt xp_a ON a.id = xp_a.id\n                INNER JOIN xp_objekt xp_b ON b.id = xp_b.id\n                INNER JOIN {short_plan_type}_bereich ON xp_a."gehoertZuBereich_id" = {short_plan_type}_bereich.id\n                                      AND xp_b."gehoertZuBereich_id" = {short_plan_type}_bereich.id\n                CROSS JOIN LATERAL (\n                    SELECT ST_CollectionExtract(\n                        ST_Intersection(a.position, b.position, {GRID_TOLERANCE}), \n                        GREATEST(st_dimension(a.position), st_dimension(b.position)) + 1\n                    ) AS polygon_geom\n                ) AS intersection_geom\n            WHERE\n                a.id < b.id\n                AND ST_IsValid(a.position)\n                AND ST_IsValid(b.position)\n                AND a.flaechenschluss = TRUE\n                AND b.flaechenschluss = TRUE\n                AND {short_plan_type}_bereich."gehoertZuPlan_id" = '{plan_id}'\n                AND (\n                    ST_Overlaps(a.position, b.position)\n                    OR st_within(a.position, b.position)\n                )\n                AND NOT ST_IsEmpty(polygon_geom);\n        """)
        res = session.execute(stmt).all()
        for row in res:
            error_type = GeometryIntersectionType.FullyWithin if row.is_within else GeometryIntersectionType.Planinhalt
            validation_result = ValidationResult(xid=str(row.a_xid), xtype=table_name_to_class(row.a_type), geom_wkt=row.wkt, intersection_type=error_type, other_xid=str(row.b_xid), other_xtype=table_name_to_class(row.b_type))
            result.append(validation_result)
        return result

def _validate_within_bounds(plan_id, short_plan_type: str) -> List[ValidationResult]:
    """ validate if all geometries of plan contents are within the bounds of the plan """
    result = []
    with Session() as session:
        stmt = text(f'\n            SELECT\n                ST_AsText(\n                    ST_CollectionExtract(\n                        ST_Difference(xp_bereich.geltungsbereich, xp_plan."raeumlicherGeltungsbereich")\n                    )\n                ) as wkt,\n                xp_bereich.id as bereich_id,\n                xp_bereich.type as bereich_type,\n                xp_plan.id as plan_id,\n                xp_plan.type as plan_type\n            FROM {short_plan_type}_bereich\n            JOIN xp_bereich ON {short_plan_type}_bereich.id = xp_bereich.id\n            JOIN xp_plan ON xp_plan.id = {short_plan_type}_bereich."gehoertZuPlan_id"\n            WHERE\n                xp_plan.id = :planid AND\n                ST_IsValid(xp_bereich.geltungsbereich) AND\n                NOT st_coveredby(xp_bereich.geltungsbereich, st_buffer(xp_plan."raeumlicherGeltungsbereich", {GRID_TOLERANCE}));\n        ')
        stmt = stmt.bindparams(planid=plan_id)
        res = session.execute(stmt).all()
        for row in res:
            validation_result = ValidationResult(xid=str(row.bereich_id), xtype=table_name_to_class(row.bereich_type), geom_wkt=row.wkt, intersection_type=GeometryIntersectionType.Plan, other_xid=str(row.plan_id), other_xtype=table_name_to_class(row.plan_type))
            result.append(validation_result)
        stmt = text(f'\n            WITH all_objekt_positions AS (\n                SELECT id, position FROM bp_objekt\n                UNION ALL\n                SELECT id, position FROM fp_objekt\n                UNION ALL\n                SELECT id, position FROM lp_objekt\n                UNION ALL\n                SELECT id, position FROM so_objekt\n            )\n            SELECT\n                ST_AsText(\n                    ST_CollectionExtract(\n                        ST_Difference(a.position, xp_bereich.geltungsbereich)\n                    )\n                )  as wkt,\n                xp_a.id AS a_xid,\n                xp_a.type AS a_type,\n                {short_plan_type}_bereich."gehoertZuPlan_id" AS plan_id,\n                xp_bereich.id AS bereich_id,\n                xp_bereich.type as bereich_type\n            FROM all_objekt_positions a\n            JOIN xp_objekt xp_a ON a.id = xp_a.id\n            JOIN {short_plan_type}_bereich ON {short_plan_type}_bereich.id = xp_a."gehoertZuBereich_id"\n            JOIN xp_bereich ON {short_plan_type}_bereich.id = xp_bereich.id\n            WHERE\n                {short_plan_type}_bereich."gehoertZuPlan_id" = :planid AND\n                ST_IsValid(a.position) AND\n                NOT st_coveredby(a.position, st_buffer(xp_bereich.geltungsbereich, {GRID_TOLERANCE}));\n        ')
        stmt = stmt.bindparams(planid=plan_id)
        res = session.execute(stmt).all()
        for row in res:
            validation_result = ValidationResult(xid=str(row.a_xid), xtype=table_name_to_class(row.a_type), geom_wkt=row.wkt, intersection_type=GeometryIntersectionType.Bereich, other_xid=str(row.bereich_id), other_xtype=table_name_to_class(f'{short_plan_type}_bereich'))
            result.append(validation_result)
        return result

def _validate_geometry_valid(plan_id, short_plan_type: str) -> List[ValidationResult]:
    result = []
    with Session() as session:
        stmt = text(f"""\n            WITH all_objekt_positions AS (\n                SELECT id, flaechenschluss, position FROM bp_objekt\n                UNION ALL\n                SELECT id, flaechenschluss, position FROM fp_objekt\n                UNION ALL\n                SELECT id, flaechenschluss, position FROM lp_objekt\n                UNION ALL\n                SELECT id, flaechenschluss, position FROM so_objekt\n            ),\n            bereiche AS (\n                SELECT\n                    xp_bereich.id,\n                    xp_bereich.geltungsbereich,\n                    xp_bereich.type\n                FROM xp_bereich\n                JOIN {short_plan_type}_bereich ON {short_plan_type}_bereich.id = xp_bereich.id\n                WHERE {short_plan_type}_bereich."gehoertZuPlan_id" = :planid\n            ),\n            objects AS (\n                SELECT\n                    o.id,\n                    o.position,\n                    xp_objekt.type\n                FROM all_objekt_positions o\n                JOIN xp_objekt ON o.id = xp_objekt.id\n                JOIN bereiche b ON b.id = xp_objekt."gehoertZuBereich_id"\n            )\n            SELECT\n                id,\n                ST_AsText(geom) as wkt,\n                type,\n                ST_IsValid(geom) AS is_valid,\n                ST_IsValidReason(geom) AS invalid_reason,\n                NOT ST_OrderingEquals(geom, ST_RemoveRepeatedPoints(geom)) AS has_duplicate_vertices,\n                ST_IsPolygonCCW(geom) AS is_ccw\n            FROM (\n                SELECT id, position AS geom, type FROM objects\n                UNION ALL\n                SELECT id, geltungsbereich AS geom, type FROM bereiche\n                UNION ALL\n                SELECT id, "raeumlicherGeltungsbereich" AS geom, 'xp_plan' FROM xp_plan\n                WHERE xp_plan.id = :planid\n            ) AS all_geometries;\n        """)
        stmt = stmt.bindparams(planid=plan_id)
        res = session.execute(stmt).all()
        for row in res:
            if row.is_valid is False:
                validation_result = ValidationResult(xid=str(row.id), xtype=table_name_to_class(row.type), geom_wkt=row.wkt, error_msg=row.invalid_reason)
                result.append(validation_result)
            if row.is_ccw is False:
                validation_result = ValidationResult(xid=str(row.id), xtype=table_name_to_class(row.type), geom_wkt=row.wkt, error_msg='Falscher Polygon-Umlaufsinn')
                result.append(validation_result)
            if row.has_duplicate_vertices is True:
                validation_result = ValidationResult(xid=str(row.id), xtype=table_name_to_class(row.type), geom_wkt=row.wkt, error_msg='Planinhalt besitzt doppelte Stützpunkte')
                result.append(validation_result)
        return result

def _validate_gaps(plan_id, short_plan_type: str) -> List[ValidationResult]:
    result = []
    with Session.begin() as session:
        stmt = text(f"""\n            SELECT \n                ST_AsText((ST_dump(st_difference(xp_plan."raeumlicherGeltungsbereich", plan_contents.united))).geom) as wkt, xp_plan.id\n            FROM\n                (\n                SELECT ST_union(objects.position) as united, {short_plan_type}_bereich."gehoertZuPlan_id" AS plan_id\n                FROM\n                (\n                    SELECT id, flaechenschluss, position FROM bp_objekt\n                    UNION ALL\n                    SELECT id, flaechenschluss, position FROM fp_objekt\n                    UNION ALL\n                    SELECT id, flaechenschluss, position FROM lp_objekt\n                    UNION ALL\n                    SELECT id, flaechenschluss, position FROM so_objekt\n                ) objects\n                INNER JOIN xp_objekt xp_a ON xp_a.id = objects.id\n                INNER JOIN {short_plan_type}_bereich ON xp_a."gehoertZuBereich_id" = {short_plan_type}_bereich.id\n                WHERE objects.flaechenschluss = TRUE AND st_isvalid(objects.position)\n                GROUP BY {short_plan_type}_bereich."gehoertZuPlan_id"\n                ) as plan_contents\n            INNER JOIN xp_plan ON plan_contents.plan_id = xp_plan.id\n            WHERE xp_plan.id = '{plan_id}';\n        """)
        res = session.execute(stmt)
        for row in res:
            validation_result = ValidationResult(xid=str(row.id), xtype=XP_Plan, geom_wkt=row.wkt, intersection_type=GeometryIntersectionType.NotCovered)
            result.append(validation_result)
        return result
VALIDATION_FUNCTIONS = [_validate_geometry_valid, _validate_within_bounds, _validate_overlaps, _validate_gaps]