import json
import os
import tempfile
import webbrowser
from datetime import datetime, timedelta

from qgis.core import (
    QgsProcessing, QgsProcessingAlgorithm,
    QgsProcessingParameterFeatureSource, QgsProcessingParameterField,
    QgsProcessingParameterString, QgsCoordinateReferenceSystem, 
    QgsCoordinateTransform, QgsGeometry
)

from .common import DEFAULT_REF_DATE

class SpaceTimeCubeViewerAlg(QgsProcessingAlgorithm):
    INPUT_LAYER = 'INPUT_LAYER'
    FIELD_ID = 'FIELD_ID'
    FIELD_X = 'FIELD_X'
    FIELD_Y = 'FIELD_Y'
    FIELD_Z = 'FIELD_Z'
    BASE_DATE = 'BASE_DATE'

    def name(self): return 'space_time_cube_interactive'
    def displayName(self): return '3. Visualization: Interactive Space-Time Cube'
    def group(self): return 'Shoreline Analysis'
    def groupId(self): return 'shoreline'
    def createInstance(self): return SpaceTimeCubeViewerAlg()

    def shortHelpString(self):
        return (
            "<h3>Step 4 — Interactive space-time cube</h3>"
            "<p>Builds an offline Plotly HTML page (3D space-time cube linked to a 2D "
            "OpenStreetMap view) from the point layer and opens it in your browser. "
            "Plotly is bundled, so no internet connection is required.</p>"
        )

    def initAlgorithm(self, config=None):
        self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT_LAYER, 'Vector Layer (Points)', [QgsProcessing.TypeVectorPoint]))
        self.addParameter(QgsProcessingParameterField(self.FIELD_ID, 'Transect ID field', 'longi', self.INPUT_LAYER, QgsProcessingParameterField.Any))
        self.addParameter(QgsProcessingParameterField(self.FIELD_X, 'Along-shore field (X / longi)', 'longi', self.INPUT_LAYER, QgsProcessingParameterField.Any))
        self.addParameter(QgsProcessingParameterField(self.FIELD_Y, 'Width field (Y / dist_m)', 'dist_m', self.INPUT_LAYER, QgsProcessingParameterField.Numeric))
        self.addParameter(QgsProcessingParameterField(self.FIELD_Z, 'Time field [days] (Z)', 'days', self.INPUT_LAYER, QgsProcessingParameterField.Any))
        self.addParameter(QgsProcessingParameterString(self.BASE_DATE, 'Base Date (YYYY-MM-DD)', defaultValue=DEFAULT_REF_DATE))

    def processAlgorithm(self, parameters, context, feedback):
        layer = self.parameterAsSource(parameters, self.INPUT_LAYER, context)
        fld_id = self.parameterAsString(parameters, self.FIELD_ID, context)
        fld_x = self.parameterAsString(parameters, self.FIELD_X, context)
        fld_y = self.parameterAsString(parameters, self.FIELD_Y, context)
        fld_z = self.parameterAsString(parameters, self.FIELD_Z, context)
        base_date_str = self.parameterAsString(parameters, self.BASE_DATE, context)

        try:
            START_DATE = datetime.strptime(base_date_str, '%Y-%m-%d')
        except ValueError:
            raise Exception("Base Date must be in YYYY-MM-DD format.")

        feedback.pushInfo(f"Processing layer: {layer.sourceName()}...")

        crs_src = layer.sourceCrs()
        crs_dest = QgsCoordinateReferenceSystem("EPSG:4326")
        transform = QgsCoordinateTransform(crs_src, crs_dest, context.transformContext())

        data_grid = {}
        longi_set = set()
        id_mapping = {}
        
        point_coords = {}    
        transect_coords = {} 

        total_features = layer.featureCount()
        step = total_features / 100.0 if total_features > 0 else 1
        skipped = 0

        for current, feat in enumerate(layer.getFeatures()):
            if feedback.isCanceled():
                break
                
            try:
                val_longi = float(feat[fld_x])
                val_days = int(feat[fld_z])
                val_dist = float(feat[fld_y])
                val_id = str(feat[fld_id])
                
                if val_days not in data_grid:
                    data_grid[val_days] = {}
                    point_coords[val_days] = {}
                    
                data_grid[val_days][val_longi] = val_dist
                longi_set.add(val_longi)
                id_mapping[val_longi] = val_id

                geom = feat.geometry()
                if not geom.isNull():
                    geom_clone = QgsGeometry(geom)
                    geom_clone.transform(transform)
                    pt = geom_clone.centroid().asPoint()
                    
                    point_coords[val_days][val_longi] = (pt.y(), pt.x())
                    
                    if val_longi not in transect_coords:
                        transect_coords[val_longi] = []
                    transect_coords[val_longi].append((val_dist, pt.y(), pt.x()))

            except (ValueError, TypeError, KeyError):  # invalid attribute values
                skipped += 1
                continue

            if current % max(1, int(step)) == 0:
                feedback.setProgress(int(current / step))

        if skipped:
            feedback.pushInfo(f"Skipped {skipped} feature(s) with invalid attribute values.")
        if not data_grid:
            raise Exception("Error: No valid data extracted. Please check your selected fields.")

        sorted_days = sorted(list(data_grid.keys()))
        sorted_longi = sorted(list(longi_set))

        feedback.pushInfo("Extracting 1 shoreline per month...")
        selected_days = []
        seen_months = set()
        
        for d in sorted_days:
            date_obj = START_DATE + timedelta(days=d)
            month_key = f"{date_obj.year}-{date_obj.month:02d}"
            if month_key not in seen_months:
                seen_months.add(month_key)
                selected_days.append(d)

        shoreline_lat, shoreline_lon = [], []
        for d in selected_days:
            longis_for_day = sorted(list(point_coords[d].keys()))
            for l in longis_for_day:
                shoreline_lat.append(point_coords[d][l][0])
                shoreline_lon.append(point_coords[d][l][1])
            shoreline_lat.append(None) 
            shoreline_lon.append(None)

        map_lat_segments = []
        map_lon_segments = []
        map_longi_array = []

        for l in sorted_longi:
            if l in transect_coords:
                pts = transect_coords[l]
                pts.sort(key=lambda x: x[0]) 
                lat_start, lon_start = pts[0][1], pts[0][2]
                lat_end, lon_end = pts[-1][1], pts[-1][2]
                map_lat_segments.append([lat_start, lat_end])
                map_lon_segments.append([lon_start, lon_end])
                map_longi_array.append(l)

        baseline_widths = {}
        first_day = sorted_days[0]
        for l in sorted_longi:
            baseline_widths[l] = data_grid[first_day].get(l, 0)

        x_matrix, y_matrix, z_matrix, color_matrix, text_matrix = [], [], [], [], []

        for d in sorted_days:
            row_x, row_y, row_z, row_color, row_text = [], [], [], [], []
            current_date = (START_DATE + timedelta(days=d)).strftime('%Y-%m-%d')
            
            for l in sorted_longi:
                dist = data_grid[d].get(l, None)
                transect_id = id_mapping.get(l, "N/A")
                
                row_x.append(l)
                row_z.append(d)
                
                if dist is not None:
                    change = dist - baseline_widths[l]
                    row_y.append(dist)
                    row_color.append(change)
                    row_text.append(
                        f"Date: {current_date}<br>"
                        f"Transect ID: <b>{transect_id}</b><br>"
                        f"Along-shore: {l}<br>"
                        f"Width: {dist:.2f} m<br>"
                        f"Change: {change:.2f} m"
                    )
                else:
                    row_y.append(None)
                    row_color.append(None)
                    row_text.append("")
                    
            x_matrix.append(row_x)
            y_matrix.append(row_y)
            z_matrix.append(row_z)
            color_matrix.append(row_color)
            text_matrix.append(row_text)

        center_lat = map_lat_segments[len(map_lat_segments)//2][0] if map_lat_segments else 0
        center_lon = map_lon_segments[len(map_lon_segments)//2][0] if map_lon_segments else 0

        feedback.pushInfo("Generating HTML...")

        # Bundle Plotly inline so the cube works without internet access.
        plotly_path = os.path.join(os.path.dirname(__file__), '..', 'resources', 'plotly.min.js')
        try:
            with open(plotly_path, 'r', encoding='utf-8') as pf:
                plotly_js = pf.read()
            plotly_tag = f"<script>{plotly_js}</script>"
        except OSError:
            feedback.pushWarning("Bundled plotly.min.js not found; falling back to CDN (internet required).")
            plotly_tag = '<script src="https://cdn.plot.ly/plotly-2.32.0.min.js"></script>'

        shoreline_hover_text = []
        for d in selected_days:
            date_str = (START_DATE + timedelta(days=d)).strftime('%Y-%m-%d')
            shoreline_hover_text.append(f"Date: {date_str}<br>Type: Monthly Shoreline")

        html_content = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <title>Shoreline Trends Dynamics</title>
            {plotly_tag}
            <style>
                body {{ font-family: Arial, sans-serif; margin: 0; padding: 10px; background-color: #f5f5f5; }}
                .header {{ text-align: center; margin-bottom: 10px; }}
                .container {{ display: flex; justify-content: space-between; height: 88vh; width: 100%; }}
                .map-box {{ width: 35%; background: #fff; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }}
                .cube-box {{ width: 63%; background: #fff; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }}
            </style>
        </head>
        <body>
            <div class="header">
                <h2>Shoreline Trends (1 Shoreline / Month) & 3D Cube</h2>
            </div>
            <div class="container">
                <div id="map2d" class="map-box"></div>
                <div id="cube3d" class="cube-box"></div>
            </div>

            <script>
                // === 3D CUBE SETUP ===
                var trace3d = {{
                    type: 'surface',
                    x: {json.dumps(x_matrix)},
                    y: {json.dumps(y_matrix)},
                    z: {json.dumps(z_matrix)},
                    surfacecolor: {json.dumps(color_matrix)},
                    text: {json.dumps(text_matrix)},
                    hoverinfo: 'text',
                    colorscale: 'Jet',
                    colorbar: {{ title: 'Change (m)', thickness: 15, len: 0.8 }}
                }};

                var layout3d = {{
                    title: '3D Space-Time Cube',
                    scene: {{
                        xaxis: {{ title: 'Along-shore distance (m)' }},
                        yaxis: {{ title: 'Beach Width (m)' }},
                        zaxis: {{ title: 'Time (days)' }},
                        aspectmode: 'manual',
                        aspectratio: {{ x: 2, y: 1, z: 1 }}
                    }},
                    margin: {{ l: 10, r: 10, b: 10, t: 40 }},
                    hovermode: 'closest'
                }};

                Plotly.newPlot('cube3d', [trace3d], layout3d);

                // === 2D MAP SETUP ===
                var trace2d_shorelines = {{
                    type: 'scattermapbox',
                    mode: 'lines',
                    lat: {json.dumps(shoreline_lat)},
                    lon: {json.dumps(shoreline_lon)},
                    text: {json.dumps(shoreline_hover_text)},
                    hoverinfo: 'text',
                    line: {{ width: 1.5, color: '#0055ff' }},
                    opacity: 0.25,
                    name: 'Shorelines'
                }};

                var trace2d_highlight = {{
                    type: 'scattermapbox',
                    mode: 'lines',
                    lat: [],
                    lon: [],
                    line: {{ width: 4, color: 'red' }},
                    hoverinfo: 'name',
                    name: 'Active Transect'
                }};

                var layout2d = {{
                    title: 'Spatial Context (OSM)',
                    mapbox: {{
                        style: 'open-street-map',
                        center: {{ lat: {center_lat}, lon: {center_lon} }},
                        zoom: 13
                    }},
                    margin: {{ l: 10, r: 10, b: 10, t: 40 }},
                    showlegend: false
                }};

                Plotly.newPlot('map2d', [trace2d_shorelines, trace2d_highlight], layout2d);

                // === ІНТЕРАКТИВНІСТЬ ===
                var cubeDiv = document.getElementById('cube3d');
                var map_lat_segments = {json.dumps(map_lat_segments)};
                var map_lon_segments = {json.dumps(map_lon_segments)};
                var map_longi = {json.dumps(map_longi_array)};

                cubeDiv.on('plotly_hover', function(data) {{
                    var hover_x = data.points[0].x; 
                    var idx = map_longi.indexOf(hover_x);
                    if (idx !== -1) {{
                        Plotly.update('map2d', {{
                            lat: [[map_lat_segments[idx][0], map_lat_segments[idx][1]]],
                            lon: [[map_lon_segments[idx][0], map_lon_segments[idx][1]]]
                        }}, {{}}, [1]); 
                    }}
                }});

                cubeDiv.on('plotly_unhover', function(data) {{
                    Plotly.update('map2d', {{ lat: [[]], lon: [[]] }}, {{}}, [1]);
                }});
            </script>
        </body>
        </html>
        """

        fd, temp_path = tempfile.mkstemp(suffix='.html', prefix='shoreline_trends_')
        with os.fdopen(fd, 'w', encoding='utf-8') as f:
            f.write(html_content)

        # Зберігаємо шлях для відкриття в браузері
        self.html_path = temp_path
        return {}

    def postProcessAlgorithm(self, context, feedback):
        """ Відкриваємо браузер після успішної генерації """
        if hasattr(self, 'html_path') and os.path.exists(self.html_path):
            feedback.pushInfo(f"Opening browser: {self.html_path}")
            webbrowser.open('file://' + self.html_path)
        return super().postProcessAlgorithm(context, feedback)