"""KML/KMZ builder engine with configurable label size."""

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


class KmlBuilder:
    def __init__(self, config):
        self.config = config
        self.html_builder = HtmlTemplateBuilder(config)

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def build(self, layer, output_path, output_format='kml'):
        """
        Export a QGIS vector layer to KML/KMZ.

        Coordinate transform strategy (priority order):
          1. QGIS Processing native:reprojectlayer  → identical to QGIS native
             "Save As KML". Uses datum grids (NTv2/VERTCON) when available.
          2. Direct QgsCoordinateTransform fallback (if Processing unavailable).
        """
        try:
            target_crs = QgsCoordinateReferenceSystem('EPSG:4326')
            source_crs = layer.crs()

            # ── Step 1: Reproject via Processing (100% accurate) ──
            reprojected = self._reproject_via_processing(layer, source_crs,
                                                         target_crs)
            if reprojected is not None:
                # Already in WGS84 → no further transform needed
                working_layer = reprojected
                transform = None
            else:
                # Fallback: manual transform with explicit project context
                working_layer = layer
                transform = None
                if source_crs != target_crs:
                    transform = QgsCoordinateTransform(
                        source_crs, target_crs,
                        QgsProject.instance().transformContext())

            if output_format == 'kmz':
                kml_content = self._build_kml_document(working_layer, transform)
                return self._write_kmz(kml_content, output_path)
            # Stream write KML directly to file
            return self._build_and_write_kml(working_layer, transform, output_path)
        except Exception as e:
            return False, f"Error: {str(e)}"

    # ------------------------------------------------------------------
    # Reprojection (Processing-based for datum-grid accuracy)
    # ------------------------------------------------------------------

    @staticmethod
    def _reproject_via_processing(layer, source_crs, target_crs):
        """
        Reproject *layer* to *target_crs* using QGIS's native:reprojectlayer
        Processing algorithm — the same datum-grid pipeline used by
        QGIS's built-in "Save As KML" / "Export Layer" tools.

        Returns:
            QgsVectorLayer (reprojected memory layer) on success.
            None if layer already in target CRS or Processing unavailable.
        """
        if source_crs == target_crs:
            return None  # no-op: caller uses original layer with transform=None

        try:
            import processing as _proc
            result = _proc.run(
                'native:reprojectlayer',
                {
                    'INPUT': layer,
                    'TARGET_CRS': target_crs,
                    'OPERATION': '',      # auto-select best PROJ operation
                    'OUTPUT': 'TEMPORARY_OUTPUT',
                }
            )
            out = result.get('OUTPUT')
            if out is not None and out.isValid():
                return out
        except (RuntimeError, AttributeError, TypeError):
            pass  # nosec B110 — QGIS processing API may raise unpredictably

        return None

    def _build_kml_document(self, layer, transform):
        """Build KML content in memory (used for KMZ)."""
        field_names = [f.name() for f in layer.fields()]
        lines = ['<?xml version="1.0" encoding="UTF-8"?>',
                 '<kml xmlns="http://www.opengis.net/kml/2.2">',
                 '<Document>',
                 f'<name>{self._esc(layer.name())}</name>']
        lines.extend(self._build_styles())
        name_cfg = self.config.get('name_fields', {})
        cond_cfg = self.config.get('conditional_colors', {})
        for feat in layer.getFeatures():
            pm = self._build_placemark(feat, name_cfg, cond_cfg, transform, field_names)
            if pm:
                lines.extend(pm)
        lines.extend(['</Document>', '</kml>'])
        return '\n'.join(lines)

    def _build_and_write_kml(self, layer, transform, path):
        """Stream write KML directly to file instead of in-memory accumulation."""
        field_names = [f.name() for f in layer.fields()]
        name_cfg = self.config.get('name_fields', {})
        cond_cfg = self.config.get('conditional_colors', {})

        try:
            with open(path, 'w', encoding='utf-8') as f:
                f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
                f.write('<kml xmlns="http://www.opengis.net/kml/2.2">\n')
                f.write('<Document>\n')
                f.write(f'<name>{self._esc(layer.name())}</name>\n')
                for line in self._build_styles():
                    f.write(line + '\n')

                for feat in layer.getFeatures():
                    pm_lines = self._build_placemark(feat, name_cfg, cond_cfg, transform, field_names)
                    if pm_lines:
                        f.write('\n'.join(pm_lines) + '\n')

                f.write('</Document>\n</kml>\n')
            return True, f"KML saved: {path}"
        except Exception as e:
            return False, str(e)

    def _build_styles(self):
        lines = []
        poly = self.config.get('polygon_style', {})
        border_kml = hex_to_kml_color(poly.get('border_color', '#FF0000'), 100)
        fill_kml = hex_to_kml_color(
            poly.get(
                'fill_color', '#00FF00'), poly.get(
                'fill_opacity', 50))
        bw = poly.get('border_width', 2)
        name_cfg = self.config.get('name_fields', {})
        name_size = name_cfg.get('font_size', 12)
        label_scale = round(max(0.5, name_size / 12.0), 1)
        name_color = hex_to_kml_color(
            name_cfg.get('font_color', '#FFFFFF'), 100)

        lines.append('<Style id="style_default">')
        lines.append('<IconStyle><scale>0</scale></IconStyle>')
        lines.append(
            f'<LabelStyle><color>{name_color}</color><scale>{label_scale}</scale></LabelStyle>')
        lines.append(
            f'<LineStyle><color>{border_kml}</color><width>{bw}</width></LineStyle>')
        lines.append(f'<PolyStyle><color>{fill_kml}</color></PolyStyle>')
        lines.append('</Style>')

        cond = self.config.get('conditional_colors', {})
        if cond.get('enabled', False):
            for i, rule in enumerate(cond.get('rules', [])):
                rb = hex_to_kml_color(rule.get('border_color', '#FF0000'), 100)
                rf = hex_to_kml_color(
                    rule.get(
                        'fill_color', '#FF0000'), poly.get(
                        'fill_opacity', 50))
                lines.append(f'<Style id="style_rule_{i}">')
                lines.append('<IconStyle><scale>0</scale></IconStyle>')
                lines.append(
                    f'<LabelStyle><color>{name_color}</color><scale>{label_scale}</scale></LabelStyle>')
                lines.append(
                    f'<LineStyle><color>{rb}</color><width>{bw}</width></LineStyle>')
                lines.append(f'<PolyStyle><color>{rf}</color></PolyStyle>')
                lines.append('</Style>')
        return lines

    def _build_placemark(self, feat, name_cfg, cond_cfg, transform, field_names=None):
        geom = feat.geometry()
        if geom is None or geom.isEmpty():
            return []
        if transform:
            geom.transform(transform)
        name = self._build_name(feat, name_cfg)
        if field_names is not None:
            fdata = {fn: feat[fn] for fn in field_names}
        else:
            fdata = {f.name(): feat[f.name()] for f in feat.fields()}
        desc = self.html_builder.build(fdata)
        sid = self._determine_style(feat, cond_cfg)

        lines = []
        gt = QgsWkbTypes.geometryType(geom.wkbType())

        if gt == QgsWkbTypes.GeometryType.PolygonGeometry:
            # Single placemark with name + description + polygon geometry
            lines.extend(['<Placemark>',
                          f'<name>{self._esc(name)}</name>',
                          f'<styleUrl>#{sid}</styleUrl>',
                          f'<description><![CDATA[{desc}]]></description>'])
            lines.extend(self._poly_kml_geom_only(geom))
            lines.append('</Placemark>')
        elif gt == QgsWkbTypes.GeometryType.LineGeometry:
            lines.extend(['<Placemark>',
                          f'<name>{self._esc(name)}</name>',
                          f'<styleUrl>#{sid}</styleUrl>',
                          f'<description><![CDATA[{desc}]]></description>'])
            lines.extend(self._line_kml(geom))
            lines.append('</Placemark>')
        elif gt == QgsWkbTypes.GeometryType.PointGeometry:
            lines.extend(['<Placemark>',
                          f'<name>{self._esc(name)}</name>',
                          f'<styleUrl>#{sid}</styleUrl>',
                          f'<description><![CDATA[{desc}]]></description>'])
            lines.extend(self._pt_kml(geom))
            lines.append('</Placemark>')

        return lines

    def _build_name(self, feat, cfg, field_names=None):
        fnames = (field_names if field_names is not None
                  else [f.name() for f in feat.fields()])
        sep = cfg.get('separator', ' - ')

        # ── 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 'Unnamed')

        # ── Old format: field1/field2/field3 ──
        f1, f2 = cfg.get('field1', ''), 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 'Unnamed'
        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 'Unnamed'
        return sep.join(parts)

    def _determine_style(self, feat, cond_cfg, field_names=None):
        if not cond_cfg.get('enabled', False):
            return 'style_default'
        field = cond_cfg.get('field', '')
        if not field:
            return 'style_default'
        fnames = field_names if field_names is not None else [f.name() for f in feat.fields()]
        val = feat[field] if field in fnames else None
        for i, rule in enumerate(cond_cfg.get('rules', [])):
            if HtmlTemplateBuilder._evaluate_condition(
                    val, rule.get('operator', '='), rule.get('value', '')):
                return f'style_rule_{i}'
        return 'style_default'

    def _poly_kml_geom_only(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()},{p.y()},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()},{p.y()},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()},{p.y()},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()},{p.y()},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>{pt.x()},{pt.y()},0</coordinates></Point>')
            lines.append('</MultiGeometry>')
        else:
            pt = geom.asPoint()
            lines.append(
                f'<Point><coordinates>{pt.x()},{pt.y()},0</coordinates></Point>')
        return lines

    @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;')
