"""Merge multiple SHP files into a single KML/KMZ with per-layer styling.

Each SHP becomes a <Folder> inside a single <Document>.
Each layer gets its own independent style, label, and popup config.
"""

import os
import zipfile
from qgis.core import (
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform, QgsProject,
    QgsVectorLayer, QgsWkbTypes
)
from .color_utils import hex_to_kml_color
from .html_template import HtmlTemplateBuilder


class MergeKmlBuilder:
    """Build a single KML/KMZ from multiple SHP files with per-layer config."""

    GEOM_POINT = 'point'
    GEOM_LINE = 'line'
    GEOM_POLYGON = 'polygon'

    def build(self, layer_configs, output_path, output_format='kml', scale_denominator=None):
        """Build merged KML/KMZ.

        Args:
            layer_configs: list of (shp_path, config_dict).
                           Each config_dict has the same structure as before
                           (name_fields, description_fields, polygon_style, etc.)
            output_path: path for the output .kml or .kmz
            output_format: 'kml' or 'kmz'

        Returns:
            (success: bool, message: str)
        """
        try:
            target_crs = QgsCoordinateReferenceSystem('EPSG:4326')
            kml_lines = [
                '<?xml version="1.0" encoding="UTF-8"?>',
                '<kml xmlns="http://www.opengis.net/kml/2.2">',
                '<Document>',
                '<name>Merged SHP Layers</name>'
            ]

            # Pre-scan layers to build styles
            layer_infos = []
            errors = []
            for i, (shp_path, cfg) in enumerate(layer_configs):
                basename = os.path.splitext(
                    os.path.basename(shp_path))[0]
                layer = QgsVectorLayer(shp_path, basename, 'ogr')
                if not layer.isValid():
                    errors.append(f"Cannot load: {basename}")
                    layer_infos.append(None)
                    continue
                gtype = self._detect_geom_type(layer)
                if gtype is None:
                    errors.append(f"Unknown geometry: {basename}")
                    layer_infos.append(None)
                    continue
                layer_infos.append((layer, gtype, basename))
                # Build style for this layer
                style_id = f'style_{i}'
                kml_lines.extend(
                    self._build_layer_style(style_id, gtype, cfg))

            # Build placemarks
            loaded_count = 0
            for i, (shp_path, cfg) in enumerate(layer_configs):
                info = layer_infos[i]
                if info is None:
                    continue
                layer, gtype, basename = info
                style_id = f'style_{i}'

                # Reproject via Processing for 100% accurate datum transform
                working_layer = self._reproject_via_processing(layer, target_crs)
                if working_layer is None:
                    working_layer = layer
                    transform = None
                    if layer.crs() != target_crs:
                        transform = QgsCoordinateTransform(
                            layer.crs(), target_crs,
                            QgsProject.instance().transformContext())
                else:
                    transform = None

                # Apply scale-based visibility: skip features if layer is not visible at given scale
                if scale_denominator is not None and working_layer.hasScaleBasedVisibility():
                    min_denom = working_layer.minimumScale()  # most zoomed out
                    max_denom = working_layer.maximumScale()  # most zoomed in
                    layer_visible = True
                    if min_denom > 0 and scale_denominator > min_denom:
                        layer_visible = False  # too zoomed out
                    if max_denom > 0 and scale_denominator < max_denom:
                        layer_visible = False  # too zoomed in
                    if not layer_visible:
                        kml_lines.append('<Folder>')
                        kml_lines.append(f'<name>{self._esc(basename)}</name>')
                        kml_lines.append('<!-- Layer not visible at this scale -->')
                        kml_lines.append('</Folder>')
                        continue

                name_cfg = cfg.get('name_fields', {})
                popup_on = cfg.get('popup_enabled', True)
                html_builder = (HtmlTemplateBuilder(cfg)
                                if popup_on else None)

                kml_lines.append('<Folder>')
                kml_lines.append(
                    f'<name>{self._esc(basename)}</name>')

                for feat in working_layer.getFeatures():
                    pm = self._build_placemark(
                        feat, gtype, name_cfg, transform,
                        style_id, html_builder)
                    if pm:
                        kml_lines.extend(pm)

                kml_lines.append('</Folder>')
                loaded_count += 1

            kml_lines.extend(['</Document>', '</kml>'])
            kml_content = '\n'.join(kml_lines)

            if loaded_count == 0:
                err_detail = (
                    '\n'.join(errors) if errors
                    else 'No valid layers')
                return False, f"No layers exported.\n{err_detail}"

            if output_format == 'kmz':
                ok, msg = self._write_kmz(kml_content, output_path)
            else:
                ok, msg = self._write_kml(kml_content, output_path)

            if ok and errors:
                msg += "\n\nWarnings:\n" + '\n'.join(errors)

            return ok, msg

        except Exception as e:
            return False, f"Error: {str(e)}"

    def _detect_geom_type(self, layer):
        """Detect geometry type of a vector layer."""
        gt = layer.geometryType()
        if gt == QgsWkbTypes.GeometryType.PointGeometry:
            return self.GEOM_POINT
        elif gt == QgsWkbTypes.GeometryType.LineGeometry:
            return self.GEOM_LINE
        elif gt == QgsWkbTypes.GeometryType.PolygonGeometry:
            return self.GEOM_POLYGON
        return None

    def _build_layer_style(self, style_id, gtype, cfg):
        """Build KML <Style> for one layer."""
        lines = []
        name_cfg = cfg.get('name_fields', {})
        name_color = hex_to_kml_color(
            name_cfg.get('font_color', '#000000'), 100)
        name_size = name_cfg.get('font_size', 12)
        label_scale = round(max(0.5, name_size / 12.0), 1)

        if gtype == self.GEOM_POINT:
            pt_style = cfg.get('point_style', {})
            pt_color = hex_to_kml_color(
                pt_style.get('icon_color', '#FF4444'), 100)
            pt_scale = pt_style.get('icon_scale', 1.0)
            lines.append(f'<Style id="{style_id}">')
            lines.append(
                f'<IconStyle><color>{pt_color}</color>'
                f'<scale>{pt_scale}</scale></IconStyle>')
            lines.append(
                f'<LabelStyle><color>{name_color}</color>'
                f'<scale>{label_scale}</scale></LabelStyle>')
            lines.append('</Style>')

        elif gtype == self.GEOM_LINE:
            ln_style = cfg.get('line_style', {})
            ln_color = hex_to_kml_color(
                ln_style.get('line_color', '#0000FF'), 100)
            ln_width = ln_style.get('line_width', 2)
            lines.append(f'<Style id="{style_id}">')
            lines.append(
                '<IconStyle><scale>0</scale></IconStyle>')
            lines.append(
                f'<LabelStyle><color>{name_color}</color>'
                f'<scale>{label_scale}</scale></LabelStyle>')
            lines.append(
                f'<LineStyle><color>{ln_color}</color>'
                f'<width>{ln_width}</width></LineStyle>')
            lines.append('</Style>')

        elif gtype == self.GEOM_POLYGON:
            pg_style = cfg.get('polygon_style', {})
            pg_border = hex_to_kml_color(
                pg_style.get('border_color', '#FF0000'), 100)
            pg_fill = hex_to_kml_color(
                pg_style.get('fill_color', '#00FF00'),
                pg_style.get('fill_opacity', 50))
            pg_bw = pg_style.get('border_width', 2)
            lines.append(f'<Style id="{style_id}">')
            lines.append(
                '<IconStyle><scale>0</scale></IconStyle>')
            lines.append(
                f'<LabelStyle><color>{name_color}</color>'
                f'<scale>{label_scale}</scale></LabelStyle>')
            lines.append(
                f'<LineStyle><color>{pg_border}</color>'
                f'<width>{pg_bw}</width></LineStyle>')
            lines.append(
                f'<PolyStyle><color>{pg_fill}</color>'
                '</PolyStyle>')
            lines.append('</Style>')

        return lines

    def _build_placemark(self, feat, gtype, name_cfg,
                         transform, style_id, html_builder):
        """Build placemark XML lines for a feature."""
        geom = feat.geometry()
        if geom is None or geom.isEmpty():
            return []

        if transform:
            geom.transform(transform)

        label_lines = self._build_label_lines(feat, name_cfg)
        name = '\n'.join(t for t, _ in label_lines) if label_lines else ''
        fdata = {f.name(): feat[f.name()] for f in feat.fields()}
        desc = html_builder.build(fdata) if html_builder else ''

        lines = []

        if gtype == self.GEOM_POLYGON:
            # Placemark 1: Polygon shape WITH popup description
            lines.extend([
                '<Placemark>', '<name></name>',
                f'<styleUrl>#{style_id}</styleUrl>',
                f'<description><![CDATA[{desc}]]></description>'
            ])
            lines.extend(self._poly_kml(geom))
            lines.append('</Placemark>')

            # Placemark 2+: Centroid label points.
            # Google Earth does NOT render \n in <name>.
            # Solution: one Point Placemark per label line,
            # stacked vertically + shifted left to center each line.
            if name:
                # ── Robust centroid ──────────────────────────────
                centroid = geom.pointOnSurface()
                if centroid.isEmpty():
                    centroid = geom.centroid()
                if centroid.isEmpty():
                    centroid = None
                # ─────────────────────────────────────────────────
                if centroid and not centroid.isEmpty():
                    pt = centroid.asPoint()
                    # Sanity-check: skip (0,0) which means centroid failed
                    if pt.x() == 0.0 and pt.y() == 0.0:
                        centroid = None
                if centroid and not centroid.isEmpty():
                    pt = centroid.asPoint()
                    name_lines = [
                        (ln, clr) for ln, clr in label_lines
                        if ln.strip()
                    ]
                    n = len(name_lines)
                    spacing = 0.00008
                    char_deg = 0.000020
                    # Default label color/scale from layer style
                    default_color = name_cfg.get('font_color', '#ffffff')
                    name_size = name_cfg.get('font_size', 12)
                    label_scale = round(max(0.5, name_size / 12.0), 1)

                    for i, (line_text, row_color) in enumerate(name_lines):
                        y_offset = ((n - 1) / 2.0 - i) * spacing
                        ly = round(pt.y() + y_offset, 6)
                        x_shift = -(len(line_text) / 2.0) * char_deg
                        lx = round(pt.x() + x_shift, 6)
                        north = round(ly + 0.0002, 6)
                        south = round(ly - 0.0002, 6)
                        east = round(lx + 0.0002, 6)
                        west = round(lx - 0.0002, 6)

                        # Use inline <Style> when this row has a custom color
                        effective_color = row_color or default_color
                        use_inline = (
                            row_color and  # noqa: W504
                            row_color.upper() != default_color.upper())

                        lines.append('<Placemark>')
                        lines.append(
                            f'<name>{self._esc(line_text.strip())}</name>')
                        if use_inline:
                            kml_clr = hex_to_kml_color(effective_color, 100)
                            lines.append(
                                f'<Style>'
                                f'<LabelStyle><color>{kml_clr}</color>'
                                f'<scale>{label_scale}</scale></LabelStyle>'
                                f'<IconStyle><scale>0</scale></IconStyle>'
                                f'</Style>')
                        else:
                            lines.append(
                                f'<styleUrl>#{style_id}</styleUrl>')
                        lines.extend([
                            '<Region><LatLonAltBox>',
                            f'<north>{north}</north>'
                            f'<south>{south}</south>',
                            f'<east>{east}</east>'
                            f'<west>{west}</west>',
                            '</LatLonAltBox><Lod>'
                            '<minLodPixels>0</minLodPixels>'
                            '<maxLodPixels>-1</maxLodPixels>'
                            '</Lod></Region>',
                            f'<Point><coordinates>'
                            f'{lx:.6f},{ly:.6f},0'
                            '</coordinates></Point>',
                            '</Placemark>'
                        ])

        elif gtype == self.GEOM_LINE:
            lines.extend([
                '<Placemark>',
                f'<name>{self._esc(name)}</name>',
                f'<styleUrl>#{style_id}</styleUrl>',
                f'<description><![CDATA[{desc}]]></description>'
            ])
            lines.extend(self._line_kml(geom))
            lines.append('</Placemark>')

        elif gtype == self.GEOM_POINT:
            lines.extend([
                '<Placemark>',
                f'<name>{self._esc(name)}</name>',
                f'<styleUrl>#{style_id}</styleUrl>',
                f'<description><![CDATA[{desc}]]></description>'
            ])
            lines.extend(self._pt_kml(geom))
            lines.append('</Placemark>')

        return lines

    def _build_name(self, feat, cfg):
        """Build display name from field config.

        Supports new 'rows' format and old field1/2/3 format.
        Returns empty string when no data.
        """
        sep = cfg.get('separator', ' - ')
        fnames = [f.name() for f in feat.fields()]

        # ── New format: 'rows' list ──
        if 'rows' in cfg:
            row_texts = []
            for row in cfg['rows']:
                if not row.get('enabled', True):
                    continue
                parts = []
                for fname in row.get('fields', []):
                    if not fname or fname not in fnames:
                        continue
                    v = feat[fname]
                    if v is None or str(v) in (
                            'NULL', 'None', 'null'):
                        continue
                    parts.append(str(v))
                if not parts:
                    continue
                text = sep.join(parts)
                suffix = row.get('suffix', '')
                if suffix:
                    text += f' {suffix}'
                row_texts.append(text)
            return '\n'.join(row_texts) if row_texts else ''

        # ── Old format: field1/field2/field3 ──
        f1 = cfg.get('field1', '')
        f2 = cfg.get('field2', '')
        f3 = cfg.get('field3', '')
        f1_on = cfg.get('field1_enabled', True)
        f2_on = cfg.get('field2_enabled', True)
        f3_on = cfg.get('field3_enabled', False)
        layout = cfg.get('layout_rows', 1)
        v1 = (str(feat[f1])
              if f1_on and f1 in fnames and feat[f1] is not None
              else '')
        v2 = (str(feat[f2])
              if f2_on and f2 in fnames and feat[f2] is not None
              else '')
        v3 = (str(feat[f3])
              if f3_on and f3 in fnames and feat[f3] is not None
              else '')
        parts = [v for v in [v1, v2, v3] if v]
        if not parts:
            return ''
        if layout == 3:
            return '\n'.join(parts)
        if layout == 2:
            row1_parts = [v for v in [v1, v2] if v]
            row1 = sep.join(row1_parts) if row1_parts else ''
            if row1 and v3:
                return f"{row1}\n{v3}"
            return row1 or v3 or ''
        return sep.join(parts)

    def _build_label_lines(self, feat, cfg):
        """Return [(text, css_color), ...] for each enabled label row.

        css_color is the per-row color (hex) or '' if the row inherits
        the layer-level font_color.
        """
        sep = cfg.get('separator', ' - ')
        fnames = [f.name() for f in feat.fields()]

        if 'rows' in cfg:
            result = []
            for row in cfg['rows']:
                if not row.get('enabled', True):
                    continue
                parts = []
                for fname in row.get('fields', []):
                    if not fname or fname not in fnames:
                        continue
                    v = feat[fname]
                    if v is None or str(v) in ('NULL', 'None', 'null'):
                        continue
                    parts.append(str(v))
                if not parts:
                    continue
                text = sep.join(parts)
                suffix = row.get('suffix', '')
                if suffix:
                    text += f' {suffix}'
                color = row.get('color', '')  # '' = inherit
                result.append((text, color))
            return result
        else:
            # Old format: single label, no per-row color
            name = self._build_name(feat, cfg)
            return [(ln, '') for ln in name.split('\n') if ln.strip()]

    # ── Geometry writers ──────────────────────────────

    def _poly_kml(self, geom):
        lines = []
        if geom.isMultipart():
            lines.append('<MultiGeometry>')
            for part in geom.asMultiPolygon():
                lines.extend(self._single_poly(part))
            lines.append('</MultiGeometry>')
        else:
            p = geom.asPolygon()
            if p:
                lines.append('<MultiGeometry>')
                lines.extend(self._single_poly(p))
                lines.append('</MultiGeometry>')
        return lines

    def _single_poly(self, rings):
        lines = [
            '<Polygon>',
            '<outerBoundaryIs><LinearRing><coordinates>'
        ]
        lines.append(
            ' '.join(
                f'{p.x():.6f},{p.y():.6f},0' for p in rings[0]))
        lines.append(
            '</coordinates></LinearRing></outerBoundaryIs>')
        for i in range(1, len(rings)):
            lines.append(
                '<innerBoundaryIs><LinearRing><coordinates>')
            lines.append(
                ' '.join(
                    f'{p.x():.6f},{p.y():.6f},0'
                    for p in rings[i]))
            lines.append(
                '</coordinates></LinearRing></innerBoundaryIs>')
        lines.append('</Polygon>')
        return lines

    def _line_kml(self, geom):
        lines = []
        if geom.isMultipart():
            lines.append('<MultiGeometry>')
            for part in geom.asMultiPolyline():
                lines.append('<LineString><coordinates>')
                lines.append(
                    ' '.join(
                        f'{p.x():.6f},{p.y():.6f},0'
                        for p in part))
                lines.append('</coordinates></LineString>')
            lines.append('</MultiGeometry>')
        else:
            pl = geom.asPolyline()
            if pl:
                lines.append('<LineString><coordinates>')
                lines.append(
                    ' '.join(
                        f'{p.x():.6f},{p.y():.6f},0'
                        for p in pl))
                lines.append('</coordinates></LineString>')
        return lines

    def _pt_kml(self, geom):
        lines = []
        if geom.isMultipart():
            lines.append('<MultiGeometry>')
            for pt in geom.asMultiPoint():
                lines.append(
                    f'<Point><coordinates>'
                    f'{pt.x():.6f},{pt.y():.6f},0'
                    '</coordinates></Point>')
            lines.append('</MultiGeometry>')
        else:
            pt = geom.asPoint()
            lines.append(
                f'<Point><coordinates>'
                f'{pt.x():.6f},{pt.y():.6f},0'
                '</coordinates></Point>')
        return lines

    @staticmethod
    def _reproject_via_processing(layer, target_crs):
        """
        Reproject layer to target_crs using QGIS's native:reprojectlayer
        Processing algorithm — identical datum-grid pipeline to QGIS native
        "Save As KML". Falls back gracefully if Processing is unavailable.
        """
        if layer.crs() == target_crs:
            return None
        try:
            import processing as _proc
            result = _proc.run(
                'native:reprojectlayer',
                {
                    'INPUT': layer,
                    'TARGET_CRS': target_crs,
                    'OPERATION': '',
                    'OUTPUT': 'TEMPORARY_OUTPUT',
                }
            )
            out = result.get('OUTPUT')
            if out is not None and out.isValid():
                return out
        except Exception:  # nosec B110
            pass  # nosec B110
        return None

    # ── I/O helpers ───────────────────────────────────

    @staticmethod
    def _write_kml(content, path):
        try:
            with open(path, 'w', encoding='utf-8') as f:
                f.write(content)
            return True, f"KML saved: {path}"
        except Exception as e:
            return False, str(e)

    @staticmethod
    def _write_kmz(kml_content, path):
        try:
            with zipfile.ZipFile(
                    path, 'w', zipfile.ZIP_DEFLATED) as zf:
                zf.writestr('doc.kml', kml_content)
            return True, f"KMZ saved: {path}"
        except Exception as e:
            return False, str(e)

    @staticmethod
    def _esc(t):
        if t is None:
            return ''
        return str(t).replace('&', '&amp;').replace(
            '<', '&lt;').replace('>', '&gt;')
