# -*- coding: utf-8 -*-
import os
import time
import tempfile
import shutil
import urllib.request
import ssl
from qgis.core import (
    QgsProcessing,
    QgsProcessingAlgorithm,
    QgsProcessingParameterFeatureSource,
    QgsProcessingParameterString,
    QgsProcessingParameterFolderDestination,
    QgsProcessingParameterBoolean,
    QgsProcessingParameterCrs,
    QgsProcessingParameterNumber,
    QgsFeatureRequest,
    QgsGeometry,
    QgsCoordinateTransform,
    QgsProcessingException,
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransformContext
)
from qgis import processing
from .license_guard import SupabaseGuard


class DownloadDemnasPro(QgsProcessingAlgorithm):
    MODE = 'MODE'
    INDEX_DEM = 'INDEX_DEM'
    AOI = 'AOI'
    BUFFER_DISTANCE = 'BUFFER_DISTANCE'
    TOKEN = 'TOKEN'  # nosec B105
    OUTPUT_FOLDER = 'OUTPUT_FOLDER'
    OUTPUT_NAME = 'OUTPUT_NAME'
    CLIP_TO_AOI = 'CLIP_TO_AOI'
    OUTPUT_CRS = 'OUTPUT_CRS'

    def initAlgorithm(self, config=None):
        from qgis.core import QgsProcessingParameterEnum
        self.addParameter(QgsProcessingParameterEnum(
            self.MODE,
            'Mode Pengambilan Grid',
            options=['Online (Otomatis dari Server)', 'Offline (Intersect dengan Layer Index)'],
            defaultValue=0
        ))

        self.addParameter(QgsProcessingParameterFeatureSource(
            self.INDEX_DEM,
            'DEMNAS Index Grid (Hanya untuk Mode Offline)',
            [QgsProcessing.SourceType.TypeVectorPolygon],
            optional=True
        ))

        self.addParameter(QgsProcessingParameterFeatureSource(
            self.AOI,
            'Area of Interest (AOI)',
            [QgsProcessing.SourceType.TypeVectorPolygon]
        ))

        self.addParameter(QgsProcessingParameterString(
            self.TOKEN,
            'API Token'
        ))

        self.addParameter(QgsProcessingParameterFolderDestination(
            self.OUTPUT_FOLDER,
            'Output Folder'
        ))

        self.addParameter(QgsProcessingParameterString(
            self.OUTPUT_NAME,
            'Output Name (without extension)',
            defaultValue='DEMNAS_Output'
        ))

        self.addParameter(QgsProcessingParameterBoolean(
            self.CLIP_TO_AOI,
            'Clip DEM with AOI?',
            defaultValue=True
        ))

        self.addParameter(QgsProcessingParameterNumber(
            self.BUFFER_DISTANCE,
            'Buffer Distance (in AOI units)',
            type=QgsProcessingParameterNumber.Type.Double,
            defaultValue=100.0,
            optional=True
        ))

        self.addParameter(QgsProcessingParameterCrs(
            self.OUTPUT_CRS,
            'Output Coordinate System',
            optional=True
        ))

    def processAlgorithm(self, parameters, context, feedback):
        # 1. License Check
        feedback.pushInfo("Checking license...")
        is_valid, msg, info = SupabaseGuard.check_license(
            "BIG Downloader QGIS", "free"
        )
        feedback.pushInfo(msg)
        if not is_valid:
            raise Exception("License check failed. Please verify your license.")

        # 2. Get Parameters
        mode_idx = self.parameterAsEnum(parameters, self.MODE, context)
        mode = "Online" if mode_idx == 0 else "Offline"
        
        index_source = self.parameterAsSource(parameters, self.INDEX_DEM, context)
        aoi_source = self.parameterAsSource(parameters, self.AOI, context)
        buffer_distance = self.parameterAsDouble(
            parameters, self.BUFFER_DISTANCE, context
        )
        token = self.parameterAsString(parameters, self.TOKEN, context)
        output_folder = self.parameterAsString(parameters, self.OUTPUT_FOLDER, context)
        output_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context)
        clip_dem = self.parameterAsBool(parameters, self.CLIP_TO_AOI, context)
        output_crs = self.parameterAsCrs(parameters, self.OUTPUT_CRS, context)

        # Handle Temporary Output
        if output_folder == 'TEMPORARY_OUTPUT':
            output_folder = os.path.join(tempfile.gettempdir(), 'BIG_DEMNAS_Output')
            if not os.path.exists(output_folder):
                os.makedirs(output_folder)
            feedback.pushInfo(f"Using temporary folder: {output_folder}")

        if not aoi_source:
            raise Exception("Invalid AOI input layer")
            
        if mode == "Offline" and not index_source:
            raise Exception("Layer Index Grid lokal harus diisi jika menggunakan Mode Offline.")

        # 3. Find Intersecting Tiles
        feedback.pushInfo("Analyzing intersection...")
        aoi_features = list(aoi_source.getFeatures())

        # Verify for user that selection is working
        has_feat = 'Ya' if aoi_source.hasFeatures() else 'Tidak/Limit'
        feedback.pushInfo(f"Memproses {len(aoi_features)} fitur AOI ({has_feat}).")

        if not aoi_features:
            raise Exception("AOI layer contains no features")

        # Combine AOI geometries
        aoi_geom = QgsGeometry.unaryUnion([f.geometry() for f in aoi_features])

        # Temp dir for processing
        temp_dir = tempfile.mkdtemp(prefix='demnas_qgis_')
        temp_aoi_path = os.path.join(temp_dir, "temp_aoi.gpkg")

        # Apply Buffer (Project to Meters usually 3857) if needed
        if buffer_distance and buffer_distance > 0:
            feedback.pushInfo(
                f"Applying buffer: {buffer_distance} m (via EPSG:3857)..."
            )

            source_crs = aoi_source.sourceCrs()
            metric_crs = QgsCoordinateReferenceSystem("EPSG:3857")

            # Transform to Metric
            xform_to_metric = QgsCoordinateTransform(
                source_crs, metric_crs, context.project()
            )
            aoi_geom.transform(xform_to_metric)

            # Buffer
            aoi_geom = aoi_geom.buffer(buffer_distance, 5)

            # Transform back to Source
            xform_to_source = QgsCoordinateTransform(
                metric_crs, source_crs, context.project()
            )
            aoi_geom.transform(xform_to_source)

        # Create temp vector layer to save aoi_geom
        from qgis.core import QgsVectorFileWriter

        save_options = QgsVectorFileWriter.SaveVectorOptions()
        save_options.driverName = "GPKG"

        writer = QgsVectorFileWriter.create(
            temp_aoi_path,
            aoi_source.fields(),
            aoi_source.wkbType(),
            aoi_source.sourceCrs(),
            QgsCoordinateTransformContext(),
            save_options
        )

        if writer.hasError() != QgsVectorFileWriter.WriterError.NoError:
            raise Exception(f"Error creating temp AOI: {writer.errorMessage()}")

        # Add feature
        from qgis.core import QgsFeature
        feat = QgsFeature()
        feat.setGeometry(aoi_geom)
        writer.addFeature(feat)
        del writer  # Flush

        # Transform AOI to Index CRS for selection or WGS84 for Online Mode
        tile_names = []
        if mode == "Offline":
            if index_source.sourceCrs() != aoi_source.sourceCrs():
                xform = QgsCoordinateTransform(
                    aoi_source.sourceCrs(), index_source.sourceCrs(), context.project()
                )
                aoi_geom.transform(xform)

            # Spatial query
            request = QgsFeatureRequest().setFilterRect(aoi_geom.boundingBox())
            for feat in index_source.getFeatures(request):
                if feat.geometry().intersects(aoi_geom):
                    # Try finding name field
                    name_idx = feat.fieldNameIndex('Name')
                    if name_idx == -1:
                        name_idx = feat.fieldNameIndex('NAME')
                    if name_idx == -1:
                        name_idx = feat.fieldNameIndex('name')

                    if name_idx != -1:
                        tile_names.append(feat.attributes()[name_idx])
        else:
            # Online Mode
            feedback.pushInfo("Mengambil data Index DEMNAS secara Online...")
            import json
            
            # Project AOI to WGS84 for GeoJSON intersection
            wgs84_crs = QgsCoordinateReferenceSystem("EPSG:4326")
            if aoi_source.sourceCrs() != wgs84_crs:
                xform = QgsCoordinateTransform(
                    aoi_source.sourceCrs(), wgs84_crs, context.project()
                )
                aoi_geom.transform(xform)
            
            bbox = aoi_geom.boundingBox()
            aoi_xmin, aoi_ymin, aoi_xmax, aoi_ymax = bbox.xMinimum(), bbox.yMinimum(), bbox.xMaximum(), bbox.yMaximum()
            
            url = "https://tanahair.indonesia.go.id/portal-web/demnas.json"
            req = urllib.request.Request(url)
            
            ctx = ssl.create_default_context()
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE
            
            with urllib.request.urlopen(req, context=ctx, timeout=60) as response:  # nosec B310
                res_text = response.read().decode('utf-8')
                data = json.loads(res_text)
                
            for feat in data.get('features', []):
                geom = feat.get('geometry', {})
                if not geom: continue
                coords = geom.get('coordinates', [])
                if not coords: continue
                
                xs = []
                ys = []
                for ring in coords:
                    for pt in ring:
                        xs.append(pt[0])
                        ys.append(pt[1])
                        
                if not xs or not ys: continue
                feat_xmin, feat_xmax = min(xs), max(xs)
                feat_ymin, feat_ymax = min(ys), max(ys)
                
                if not (feat_xmax < aoi_xmin or feat_xmin > aoi_xmax or feat_ymax < aoi_ymin or feat_ymin > aoi_ymax):
                    namobj = feat.get('properties', {}).get('NAMOBJ')
                    if namobj:
                        tile_names.append(namobj)

        feedback.pushInfo(f"Ditemukan {len(tile_names)} tile untuk diunduh.")
        if not tile_names:
            return {}

        # 4. Download Tiles
        feedback.pushInfo("\n=== MENGUNDUH TILE ===")
        downloaded_files = []

        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE

        total = len(tile_names)
        feedback.pushInfo(f"Memulai unduhan sequential ({total} tiles)...")
        
        for i, tile_name in enumerate(tile_names):
            if feedback.isCanceled():
                break
                
            base_name = tile_name
            if base_name.startswith("DEMNAS_"):
                base_name = base_name.replace("DEMNAS_", "")
            if base_name.endswith("_v1.0"):
                base_name = base_name.replace("_v1.0", "")
                
            actual_filename = f"DEMNAS_{base_name}_v1.0.tif"
            base_url = "https://tanahair.indonesia.go.id/api-inageo/unduh/demnas"
            url = f"{base_url}?token={token}&filename={actual_filename}"
            out_file = os.path.join(temp_dir, f"{actual_filename}")

            max_retries = 3
            retry_count = 0
            success = False
            error_msg = ""
            
            while retry_count < max_retries:
                if feedback.isCanceled():
                    break
                try:
                    headers = {'User-Agent': 'Mozilla/5.0'}
                    current_size = 0
                    
                    if os.path.exists(out_file) and retry_count > 0:
                        current_size = os.path.getsize(out_file)
                        if current_size > 0:
                            headers['Range'] = f"bytes={current_size}-"
                            
                    req = urllib.request.Request(url, headers=headers)
                    with urllib.request.urlopen(req, context=ctx, timeout=7200) as response:  # nosec B310
                        if response.status not in (200, 206):
                            raise Exception(f"HTTP Error {response.status}")
                            
                        mode = 'ab' if response.status == 206 else 'wb'
                        
                        with open(out_file, mode) as f:
                            while True:
                                if feedback.isCanceled():
                                    break
                                chunk = response.read(65536)
                                if not chunk:
                                    break
                                f.write(chunk)

                    if os.path.exists(out_file) and os.path.getsize(out_file) > 0:
                        success = True
                        break
                    else:
                        raise Exception("File Kosong")

                except Exception as e:
                    error_msg = str(e)
                    retry_count += 1
                    if retry_count < max_retries:
                        time.sleep(3 * retry_count)
            
            if success:
                downloaded_files.append(out_file)
                feedback.pushInfo(f"Berhasil: {tile_name}")
            else:
                feedback.reportError(f"Gagal mengunduh {tile_name}: {error_msg}")
            
            feedback.setProgress(int(((i + 1) / total) * 100))

        if not downloaded_files:
            raise Exception("Tidak ada tile yang berhasil diunduh.")

        # 5. Mosaic
        feedback.pushInfo("\n=== MEMBUAT MOSAIK ===")
        mosaic_out = os.path.join(temp_dir, "mosaic.tif")

        from osgeo import gdal

        feedback.pushInfo("Merging tiles with GDAL Warp...")
        try:
            target_crs_auth = 'EPSG:4326'
            if output_crs.isValid():
                target_crs_auth = output_crs.authid()

            warp_opts = gdal.WarpOptions(
                format='GTiff',
                outputType=gdal.GDT_Float32,
                dstNodata=-9999,
                resampleAlg=gdal.GRA_Bilinear,
                srcSRS='EPSG:4326',
                dstSRS=target_crs_auth
            )
            gdal.Warp(mosaic_out, downloaded_files, options=warp_opts)

            final_raster = mosaic_out

        except Exception as e:
            raise QgsProcessingException(f"Gagal mosaik DEMNAS: {e}")

        # 6. Clip / Reproject / Save
        feedback.pushInfo("\n=== MENYIMPAN OUTPUT ===")

        if output_folder == 'TEMPORARY_OUTPUT':
            output_folder = os.path.join(tempfile.gettempdir(), 'BIG_DEMNAS_Output')
            feedback.pushInfo(f"Info: Menggunakan folder sementara: {output_folder}")

        if not os.path.exists(output_folder):
            try:
                os.makedirs(output_folder, exist_ok=True)
            except Exception as e:
                feedback.pushInfo(f"Peringatan membuat folder: {e}")

        final_name = output_name

        if clip_dem:
            final_name += "_clip"

        if output_crs.isValid():
            if output_crs.authid() != "EPSG:4326":
                final_name += "_utm"

        dest_path = os.path.join(output_folder, f"{final_name}.tif")

        if clip_dem:
            feedback.pushInfo("Menyiapkan AOI untuk clipping...")

            from qgis.core import QgsRasterLayer
            raster_layer = QgsRasterLayer(final_raster, "temp")
            raster_crs = raster_layer.crs()

            reproj_aoi = os.path.join(temp_dir, "aoi_mask_reprojected.gpkg")

            reproject_params = {
                'INPUT': temp_aoi_path,
                'TARGET_CRS': raster_crs,
                'OUTPUT': reproj_aoi
            }
            processing.run(
                "native:reprojectlayer", reproject_params, context=context
            )

            feedback.pushInfo("Memotong raster sesuai AOI...")

            clip_params = {
                'INPUT': final_raster,
                'MASK': reproj_aoi,
                'SOURCE_CRS': raster_crs,
                'TARGET_CRS': raster_crs,
                'NODATA': -9999,
                'ALPHA_BAND': False,
                'CROP_TO_CUTLINE': True,
                'KEEP_RESOLUTION': True,
                'OPTIONS': '',
                'DATA_TYPE': 0,
                'OUTPUT': dest_path
            }
            try:
                processing.run(
                    "gdal:cliprasterbymasklayer", clip_params, context=context
                )
            except Exception as e:
                feedback.reportError(f"Gagal saat clipping: {e}")
                raise e

        else:
            feedback.pushInfo("Menyalin output...")
            shutil.copy2(final_raster, dest_path)

        if os.path.exists(temp_dir):
            for i in range(3):
                try:
                    shutil.rmtree(temp_dir, ignore_errors=True)
                    if not os.path.exists(temp_dir):
                        break
                    time.sleep(1)
                except Exception:
                    time.sleep(1)

        feedback.pushInfo(f"Selesai! Output tersimpan di: {dest_path}")

        if os.path.exists(dest_path):
            from qgis.core import QgsProcessingContext
            details = QgsProcessingContext.LayerDetails(
                final_name, context.project(), final_name
            )
            context.addLayerToLoadOnCompletion(dest_path, details)

        return {self.OUTPUT_NAME: dest_path}

    def name(self):
        return 'download_demnas_pro'

    def displayName(self):
        return '2. Download DEMNAS (Mosaic)'

    def group(self):
        return 'BIG Downloader Tools'

    def groupId(self):
        return 'big_downloader_v2'

    def shortHelpString(self):
        return (
            "Download data DEM (Digital Elevation Model) Nasional - DEMNAS "
            "langsung dari server BIG.\n\n"
            "Fitur:\n"
            "- Download otomatis berdasarkan Area of Interest (AOI)\n"
            "- Mendukung filter nama grid\n"
            "- Otomatis menggabungkan (mosaic) hasil download\n"
            "- Opsi untuk memotong (clip) raster sesuai dengan batas poligon AOI"
        )

    def icon(self):
        import os
        from qgis.PyQt.QtGui import QIcon
        icon_path = os.path.join(os.path.dirname(__file__), '..', 'images', 'icon_demnas.png')
        return QIcon(icon_path)

    def createInstance(self):
        return DownloadDemnasPro()
