"""
/***************************************************************************
                                 A QGIS plugin
 Набор инструментов для работы с материалами документации территориального планирования
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2024-03-18
        git sha              : $Format:%H$
        copyright            : (C) 2024 by I.Bukharin/Mirproekt
        email                : iabukharin@gmail.com
 ***************************************************************************/
 Функции корректировки слоев
"""


import uuid
import re
from collections import defaultdict
from qgis.core import QgsEllipsoidUtils, QgsDistanceArea, QgsProject, NULL, QgsFeatureRequest
from qgis.PyQt.QtCore import QVariant
from copy import deepcopy

from qgis.core import Qgis, QgsSpatialIndex

# return count of corrected features or -1 если нет GLOBALID, -2 если GLOBALID не символьное
def autoCorrectGlobalID(layer):
    correctedFeatNumber = 0
    uuidPatterns = '(urn:uuid:)?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|\{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\}'
    
    fields = layer.fields()
    if 'GLOBALID' not in fields.names():
        return -1
    if fields['GLOBALID'].type() != QVariant.String:
        return -2
    
    indxGID = fields.indexFromName('GLOBALID')

    # Контейнеры для поиска дубликатов: {значение_uuid: [list_of_feature_ids]}
    val_map = defaultdict(list)
    layer.startEditing()
    
    # 1. Первый проход: валидация формата и сбор данных для проверки уникальности
    # Используем запрос только нужного поля для скорости
    request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes([indxGID])
    
    for feat in layer.getFeatures(request):
        f_id = feat.id()
        val = str(feat[indxGID]) if feat[indxGID] is not None else ""
        
        # Проверка на пустые значения и соответствие формату UUID
        if not val or not re.fullmatch(uuidPatterns, val):
            new_uuid = str(uuid.uuid4())
            layer.changeAttributeValue(f_id, indxGID, new_uuid)
            correctedFeatNumber += 1
            val_map[new_uuid].append(f_id)
        else:
            val_map[val].append(f_id)

    # 2. Второй проход: исправление дубликатов
    for val, ids in val_map.items():
        if len(ids) > 1:
            # Оставляем первый ID как есть, остальные (ids[1:]) меняем
            for f_id in ids[1:]:
                layer.changeAttributeValue(f_id, indxGID, str(uuid.uuid4()))
                correctedFeatNumber += 1

    return correctedFeatNumber

# return count of deleted features or 0
def deleteFeatureWithoutGeometry(layer):
    # Список для хранения ID объектов без геометрии
    idxFeatureWithoutGeometryList = []
    
    # Оптимизированный запрос: не загружаем атрибуты, только геометрию
    request = QgsFeatureRequest().setNoAttributes()
    
    for feat in layer.getFeatures(request):
        # Если геометрия отсутствует или пустая (например, NULL в базе)
        if not feat.hasGeometry() or feat.geometry().isEmpty():
            idxFeatureWithoutGeometryList.append(feat.id())
    
    # Исправлено условие: проверяем, найдены ли объекты
    if not idxFeatureWithoutGeometryList:
        return 0
    
    # Удаление объектов
    layer.startEditing()
    layer.deleteFeatures(idxFeatureWithoutGeometryList)
    
    # Возвращаем количество удаленных объектов
    return len(idxFeatureWithoutGeometryList)

# return count of corrected features or -1 если слой точечный, -2 если нет AREA или LENGTH, -3 если ошибка СК
def fillGeometrysAtttribs(layer):
    correctedFeatNumber = 0

    layerGeomType = str(layer.geometryType()).replace('GeometryType.', '')

    if layerGeomType == 'Point' or layerGeomType == '0':
        return -1
    
    fields_names = layer.fields().names()
    
    # Проверка наличия нужного поля
    target_field = 'AREA' if 'AREA' in fields_names else ''
    target_field = 'LENGTH' if 'LENGTH' in fields_names else ''

    if target_field not in fields_names:
        return -2
        
    layerCrs = layer.crs()
    if not layerCrs.isValid():
        return -3
    
    distanceArea = QgsDistanceArea()
    distanceArea.setSourceCrs(layerCrs, QgsProject.instance().transformContext())
    distanceArea.setEllipsoid(QgsProject.instance().ellipsoid())

    field_idx = layer.fields().indexFromName(target_field)

    # distanceArea.setEllipsoid(ellipsoidAcronym)
    attr_map = {}
    layer.startEditing()
    request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoAttributes)
    if layerGeomType == 'Polygon' or layerGeomType == '2':
        for feat in layer.getFeatures(request):
            try:
                calculArea = distanceArea.convertAreaMeasurement(distanceArea.measureArea(feat.geometry()), 5)
            except TypeError:
                calculArea = distanceArea.convertAreaMeasurement(distanceArea.measureArea(feat.geometry()), Qgis.AreaUnit.AreaHectares)
            """ feat['AREA'] = round(calculArea, 4)
            layer.updateFeature(feat) """
            attr_map[feat.id()] = {field_idx: round(calculArea, 4)}
            correctedFeatNumber += 1

    elif layerGeomType == 'Line' or layerGeomType == '1':
        for feat in layer.getFeatures(request):
            try:
                calculLength = distanceArea.convertLengthMeasurement(distanceArea.measureLength(feat.geometry()), 1)
            except TypeError:
                calculLength = distanceArea.convertLengthMeasurement(distanceArea.measureLength(feat.geometry()), Qgis.DistanceUnit.DistanceKilometers)
            """ feat['LENGTH'] = round(calculLength, 3)
            layer.updateFeature(feat) """
            attr_map[feat.id()] = {field_idx: round(calculArea, round(calculLength, 3))}
            correctedFeatNumber += 1
    
    if attr_map:
        layer.dataProvider().changeAttributeValues(attr_map)
    
    layer.updateExtents()
    return correctedFeatNumber

# return count of corrected features or -1 если опорный слой ОКТМО не полигональный, -2 ошибка системы координат одного из слоев, -3 если нет поля OKTMO
def fillOKTMO(layer, oktmoLayer, defOKTMO=''):
    correctedFeatNumber = 0
    if 'OKTMO' not in layer.fields().names() or 'OKTMO' not in oktmoLayer.fields().names():
        return correctedFeatNumber -3
    
    if oktmoLayer.geometryType() != Qgis.GeometryType.Polygon:
        return -1
    
    bufferFlag = False
    if layer.crs().isValid() and oktmoLayer.crs().isValid():
        if layer.crs().isGeographic or oktmoLayer.crs().isGeographic:
            pass
        elif not oktmoLayer.crs().isGeographic:
            bufferFlag = True
    else:
        return -2
    
    indxOKTMO = layer.fields().indexFromName('OKTMO')
    indxOktmoVal = oktmoLayer.fields().indexFromName('OKTMO')

    # 2. Подготовка данных и ПРОСТРАНСТВЕННОГО ИНДЕКСА
    # Храним только ID и центроид (для точности попадания в полигон)
    unassigned_ids = {} 
    spatial_index = QgsSpatialIndex()
    
    for feat in layer.getFeatures(QgsFeatureRequest().setNoAttributes()):
        geom = feat.geometry()
        if geom.isEmpty(): continue
        # Используем центроид, чтобы объект не мог попасть в два ОКТМО сразу
        center = geom.centroid()
        unassigned_ids[feat.id()] = center
        spatial_index.addFeature(feat)

    layer.startEditing()

    # 3. Основной цикл по слою ОКТМО
    for oktmoFeat in oktmoLayer.getFeatures():
        oktmoValue = oktmoFeat[indxOktmoVal]
        if not oktmoValue: continue
        
        oktmoGeom = oktmoFeat.geometry()
        # Поиск только тех объектов, которые находятся РЯДОМ с полигоном ОКТМО
        candidate_ids = spatial_index.intersects(oktmoGeom.boundingBox())
        
        processed_in_this_step = []
        for feat_id in candidate_ids:
            if feat_id not in unassigned_ids:
                continue
                
            if unassigned_ids[feat_id].within(oktmoGeom):
                layer.changeAttributeValue(feat_id, indxOKTMO, oktmoValue)
                processed_in_this_step.append(feat_id)
                correctedFeatNumber += 1
        
        # Удаляем обработанные, чтобы не проверять их снова
        for fid in processed_in_this_step:
            del unassigned_ids[fid]
    
    # 4. Присвоение значения по умолчанию оставшимся
    if defOKTMO:
        for feat_id in unassigned_ids.keys():
            layer.changeAttributeValue(feat_id, indxOKTMO, defOKTMO)
            correctedFeatNumber += 1
    
    return correctedFeatNumber

# return count of corrected features or -1 если слой не содержит KADASTRZU, -2 опорный слой не является полигональным, -3 ошибка системы координат одного из слоев
def fillZuNumber(layer, parcelLayer, parcelNumAttribName):
    correctedFeatNumber = 0
    
    # 1. Проверки полей и типов
    fields = layer.fields()
    if 'KADASTRZU' not in fields.names() or parcelNumAttribName not in parcelLayer.fields().names():
        return -1
    if parcelLayer.geometryType() != Qgis.GeometryType.Polygon:
        return -2
    if not layer.crs().isValid() or not parcelLayer.crs().isValid():
        return -3
    
    indxZU = fields.indexFromName('KADASTRZU')
    indxParcelNum = parcelLayer.fields().indexFromName(parcelNumAttribName)

    # 2. Создание пространственного индекса для участков (ускорение в сотни раз)
    # Индекс хранит только геометрию и ID участков
    spatial_index = QgsSpatialIndex(parcelLayer.getFeatures(QgsFeatureRequest().setNoAttributes()))
    
    layer.startEditing()

    for feat in layer.getFeatures():
        geom = feat.geometry()
        if geom.isEmpty():
            continue
            
        # Центроид объекта для более точного попадания в участок
        search_point = geom.centroid()
        
        # 3. Быстрый поиск кандидатов-участков через индекс
        candidate_ids = spatial_index.intersects(search_point.boundingBox())
        
        for p_id in candidate_ids:
            # Получаем конкретный участок для точной проверки геометрии
            parcel = parcelLayer.getFeature(p_id)
            if search_point.within(parcel.geometry()):
                val = parcel[indxParcelNum]
                layer.changeAttributeValue(feat.id(), indxZU, val)
                
                # Если нужно заполнять ADDRESS (раскомментируйте при необходимости)
                # if 'ADDRESS' in fields.names() and not feat['ADDRESS']:
                #     addr_idx = fields.indexFromName('ADDRESS')
                #     layer.changeAttributeValue(feat.id(), addr_idx, val)
                
                correctedFeatNumber += 1
                break # Нашли первый подходящий участок — выходим из цикла кандидатов
                
    return correctedFeatNumber

# только обнуляет ('' или 0) для условных атрибутов, если не соответсвуют условию, -1 если в слое не найдены необходимые поля, -2 если нет условий
def nullIfAttrib(layer, complexTypeAttributesDict):
    correctedFeatNumber = 0
    attributesDict = deepcopy(complexTypeAttributesDict)
    attrForDelete = []
    for attrname in attributesDict.keys():
        if len(attributesDict[attrname].get('mirDepend', '')) < 1:
            attrForDelete.append(attrname)
            continue
        if attrname not in layer.fields().names():
            attrForDelete.append(attrname)
    for delAttr in attrForDelete:
        del attributesDict[delAttr]
    if len(attributesDict) < 1:
        return -2
    if not set(attributesDict.keys()).issubset(set(layer.fields().names())):
        return -1
    for attr, val in attributesDict.items():
        mirDependAttrName = val['mirDepend'].split('=')[0]
        mirDependAtrrVal = val['mirDepend'].split('=')[1]
        mirDependAtrrVal = mirDependAtrrVal.replace('[', '')
        mirDependAtrrVal = mirDependAtrrVal.replace(']', '')
        mirDependAtrrVal = mirDependAtrrVal.replace(' ', '')
        mirDependAtrrVal = [str(x) for x in mirDependAtrrVal.split(',')]
        attributesDict[attr]['mirDepend'] = [mirDependAttrName, mirDependAtrrVal]
    layer.startEditing()
    for feat in layer.getFeatures():
        for attrName, valueDep in attributesDict.items():
            attrNameDepend = valueDep['mirDepend'][0]
            attrValuesDepend = valueDep['mirDepend'][1]

            featFieldValueNullFlag = False
            featFieldValue = feat[attrName] 
            featureFieldType = layer.fields()[attrName].type()
            if featFieldValue is None:
                featFieldValueNullFlag = True
            elif QVariant(featFieldValue).isNull():
                # featFieldValue is NULL:
                featFieldValueNullFlag = True
            elif featureFieldType == QVariant.String:
                if featFieldValue == '' or featFieldValue in ['NULL', 'Null', ' ', '']:
                    feat[attrName] = NULL
                    layer.updateFeature(feat)
                    featFieldValueNullFlag = True
            else:
                if featFieldValue == 0:
                    feat[attrName] = NULL
                    layer.updateFeature(feat)
                    featFieldValueNullFlag = True
                elif featFieldValue == 0.0:
                    feat[attrName] = NULL
                    layer.updateFeature(feat)
                    featFieldValueNullFlag = True
                elif featFieldValue == NULL or str(featFieldValue) == 'NULL':
                    featFieldValueNullFlag = True
            if featFieldValueNullFlag:
                continue
            
            if str(feat[attrNameDepend]) not in attrValuesDepend:
                # print('null chng', attrNameDepend, attrValuesDepend, feat[attrNameDepend])
                print(feat[attrName])
                if featureFieldType == QVariant.String:
                    feat[attrName] = NULL
                    layer.updateFeature(feat)
                else:
                    feat[attrName] = NULL
                    layer.updateFeature(feat)
                correctedFeatNumber += 1
    return correctedFeatNumber