# -*- coding: utf-8 -*-
"""
CAD export logic for LVT4U DWG Converter.
Handles building output field schemas, extracting level-based SHP files,
and managing multi-file batch export.

Author: Lộc Vũ Trung / Slow Forest
License: GPL-3.0
"""
from __future__ import annotations

import os
import re
import unicodedata
from typing import Optional

from qgis.core import (
    QgsFields,
    QgsField,
    QgsVectorFileWriter,
    QgsVectorLayer,
    QgsFeature,
    QgsGeometry,
    QgsWkbTypes,
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform,
    QgsCoordinateTransformContext,
)
from ..shared.qt_compat import QVariant

# ── Optional dependency: _cad_scan (created by Phase 1+2) ─────────────────
# Fall back to inline constants/stubs so this module is importable standalone.
try:
    from ._cad_scan import (  # type: ignore[import]
        LevelStat,
        GEOM_POINT,
        GEOM_LINE,
        GEOM_POLYGON,
        GEOM_TEXT,
        GEOM_NONE,
        classify_geom_type,
        extract_color,
        extract_text,
    )
except ImportError:  # _cad_scan not yet available
    # ── Minimal stubs (removed once Phase 1+2 lands) ──────────────────────
    GEOM_POINT = 'point'
    GEOM_LINE = 'line'
    GEOM_POLYGON = 'polygon'
    GEOM_TEXT = 'text'
    GEOM_NONE = 'none'

    class LevelStat:  # type: ignore[no-redef]
        """Stub for LevelStat before _cad_scan is available."""
        pass  # nosec B110

    def classify_geom_type(wkb_type: int) -> str:  # type: ignore[misc]
        """Classify a QGIS WKB type into one of the GEOM_* constants."""
        flat = QgsWkbTypes.flatType(wkb_type)
        _map = {
            QgsWkbTypes.Type.Point: GEOM_POINT,
            QgsWkbTypes.Type.MultiPoint: GEOM_POINT,
            QgsWkbTypes.Type.LineString: GEOM_LINE,
            QgsWkbTypes.Type.MultiLineString: GEOM_LINE,
            QgsWkbTypes.Type.CircularString: GEOM_LINE,
            QgsWkbTypes.Type.CompoundCurve: GEOM_LINE,
            QgsWkbTypes.Type.Polygon: GEOM_POLYGON,
            QgsWkbTypes.Type.MultiPolygon: GEOM_POLYGON,
            QgsWkbTypes.Type.CurvePolygon: GEOM_POLYGON,
        }
        return _map.get(flat, GEOM_NONE)

    def extract_color(feat: QgsFeature):  # type: ignore[misc]
        """Extract OGR color index and hex from a feature (stub)."""
        idx = None
        hex_val = None
        for name in ('ColorIndex', 'Color', 'color', 'CLR'):
            try:
                idx = int(feat[name])
                break
            except Exception:  # nosec B110
                pass  # nosec B110
        return idx, hex_val

    def extract_text(feat: QgsFeature) -> Optional[str]:  # type: ignore[misc]
        """Extract text string from a CAD text feature (stub)."""
        for name in ('Text', 'TextString', 'Label', 'text', 'label'):
            try:
                val = feat[name]
                if val not in (None, 'NULL', ''):
                    return str(val)
            except Exception:  # nosec B110
                pass  # nosec B110
        return None


# ── Optional dependency: shared text utilities ─────────────────────────────
# Fall back to simple inline helpers if the shared modules don't exist yet.
try:
    from ..shared.text_utils import (  # type: ignore[import]
        clean_ogr_text,
        sanitize_filename,
        get_unique_path,
    )
except ImportError:
    def clean_ogr_text(text: str) -> str:  # type: ignore[misc]
        """Remove OGR surrogate / null bytes (inline stub)."""
        if not isinstance(text, str):
            return text
        # Remove surrogate escape sequences
        try:
            text = text.encode('utf-8', errors='replace').decode('utf-8')
        except Exception:  # nosec B110
            pass  # nosec B110
        return text.replace('\x00', '')

    def sanitize_filename(name: str, max_len: int = 50) -> str:  # type: ignore[misc]
        """Convert a string to a safe filename component (inline stub)."""
        # Normalise unicode (NFC)
        name = unicodedata.normalize('NFC', str(name))
        # Replace characters illegal in Windows filenames
        name = re.sub(r'[\\/:*?"<>|]', '_', name)
        # Collapse whitespace
        name = re.sub(r'\s+', '_', name).strip('_')
        return name[:max_len] if max_len else name

    def get_unique_path(path: str) -> str:  # type: ignore[misc]
        """Return a unique path by appending _N suffix if needed (inline stub)."""
        if not os.path.exists(path):
            return path
        base, ext = os.path.splitext(path)
        counter = 1
        while True:
            candidate = f'{base}_{counter}{ext}'
            if not os.path.exists(candidate):
                return candidate
            counter += 1


try:
    from ..shared.encoding_vn import convert_text  # type: ignore[import]
except ImportError:
    def convert_text(text: str, encoding_mode: str) -> str:  # type: ignore[misc]
        """Encoding conversion stub — returns text unchanged."""
        return text


# ── Geometry kind → WKB type mapping ──────────────────────────────────────
_KIND_WKB = {
    GEOM_POINT: QgsWkbTypes.Type.MultiPoint,
    GEOM_LINE: QgsWkbTypes.Type.MultiLineString,
    GEOM_POLYGON: QgsWkbTypes.Type.MultiPolygon,
    GEOM_TEXT: QgsWkbTypes.Type.Point,   # Text = insertion point
}

_KIND_LABEL_EN = {
    GEOM_POINT: 'Point',
    GEOM_LINE: 'Line',
    GEOM_POLYGON: 'Polygon',
    GEOM_TEXT: 'Text',
    GEOM_NONE: 'Other',
}

# Field names added by LVT to every output SHP
_LVT_FIELDS = [
    # (name,       QVariant type,        length)
    ('LEVEL', QVariant.String, 100),
    ('LVT_KIND', QVariant.String, 10),
    ('CLR_IDX', QVariant.Int, 0),
    ('CLR_HEX', QVariant.String, 9),
    ('TEXT_STR', QVariant.String, 254),
]

# Field names used to detect the CAD Level/Layer field in source
_LEVEL_FIELD_CANDIDATES = ('Layer', 'Level', 'layer', 'level',
                           'LAYER', 'LEVEL')


# ── Public helpers ─────────────────────────────────────────────────────────

def build_output_fields(source_fields: QgsFields, kind: str) -> QgsFields:
    """
    Build the output field schema for a SHP file.

    Schema:
        - All original OGR fields (preserved from source)
        - LEVEL    (String 100) — Level/Layer name from CAD
        - LVT_KIND (String 10)  — point / line / polygon / text
        - CLR_IDX  (Integer)    — OGR color index (DGN)
        - CLR_HEX  (String 9)   — #RRGGBB hex color
        - TEXT_STR (String 254) — text content (text layers only)

    Args:
        source_fields: QgsFields from the source layer.
        kind: Geometry kind (unused for schema; kept for API symmetry).

    Returns:
        QgsFields with original + LVT standard fields.
    """
    fields = QgsFields()

    # Copy original fields first (preserve all OGR metadata)
    for f in source_fields:
        fields.append(
            QgsField(f.name(), f.type(), f.typeName(),
                     f.length(), f.precision()))

    # Append LVT standard fields (avoid duplicates)
    existing_upper = {f.name().upper() for f in source_fields}
    for fname, ftype, flen in _LVT_FIELDS:
        if fname not in existing_upper:
            fields.append(QgsField(fname, ftype, '', flen))

    return fields


def make_output_name(level_name: str, kind: str, out_dir: str,
                     prefix: str = 'L') -> str:
    """
    Build a safe SHP output path for a level+kind combination.

    Pattern: ``{out_dir}/{prefix}{safe_level}_{KindLabel}.shp``

    Examples::

        /output/L0_Line.shp
        /output/LRanhThua_Polygon.shp

    Args:
        level_name: Raw CAD level/layer name.
        kind: Geometry kind constant (GEOM_POINT, GEOM_LINE, …).
        out_dir: Target output directory (must exist).
        prefix: Optional single-character prefix (default ``'L'``).

    Returns:
        Unique absolute path ending in ``.shp``.
    """
    safe_level = sanitize_filename(level_name, max_len=30)
    kind_label = _KIND_LABEL_EN.get(kind, 'Other')
    filename = f'{prefix}{safe_level}_{kind_label}.shp'
    path = os.path.join(out_dir, filename)
    return get_unique_path(path)


def get_batch_output_dir(file_path: str, base_out_dir: str,
                         batch_mode: bool) -> str:
    """
    Determine output directory for a file.

    - **Single-file mode**: use ``base_out_dir`` directly.
    - **Batch mode**: create a subfolder named after the file stem.

    Args:
        file_path: Full path to the source CAD file.
        base_out_dir: Root output directory.
        batch_mode: If True, create a per-file sub-directory.

    Returns:
        Absolute path to the resolved output directory.
    """
    if not batch_mode:
        return base_out_dir
    file_stem = os.path.splitext(os.path.basename(file_path))[0]
    safe_name = sanitize_filename(file_stem)
    sub_dir = os.path.join(base_out_dir, safe_name)
    os.makedirs(sub_dir, exist_ok=True)
    return sub_dir


# ── Core exporter class ────────────────────────────────────────────────────

class LevelExporter:
    """
    Exports features from a source QgsVectorLayer into per-Level SHP files.

    Each (Level × GeomKind) pair produces a separate SHP file, e.g.
    ``L0_Line.shp``, ``LRanhThua_Polygon.shp``.

    **Usage**::

        exporter = LevelExporter(
            source_layer=vlayer,
            out_dir='/path/to/output',
            target_crs=QgsCoordinateReferenceSystem('EPSG:9208'),
            selected_levels=['0', '1', 'RANH_THUA'],
            encoding_mode='TCVN3',
            extract_text=True,
        )
        results = exporter.run(progress_callback=lambda pct, msg: ...)

    Args:
        source_layer:    Source QgsVectorLayer (must be valid and loaded).
        out_dir:         Output directory (will be created if absent).
        target_crs:      Target CRS for coordinate transformation.
        selected_levels: List of level names to export (empty = export all).
        encoding_mode:   One of ``'UNICODE'`` (default), ``'TCVN3'``, etc.
        extract_text:    If True, classify text features separately.
        level_stats:     Optional pre-scanned statistics from ``_cad_scan``.
    """

    def __init__(
        self,
        source_layer: QgsVectorLayer,
        out_dir: str,
        target_crs: QgsCoordinateReferenceSystem,
        selected_levels: list,
        encoding_mode: str = 'UNICODE',
        extract_text: bool = True,
        level_stats: Optional[dict] = None,
    ):
        self.source = source_layer
        self.out_dir = out_dir
        self.target_crs = target_crs
        # Empty list = export all levels
        self.selected_levels: set = set(selected_levels) if selected_levels else set()
        self.filter_levels = bool(selected_levels)
        self.encoding_mode = encoding_mode
        self.extract_text = extract_text
        self.level_stats = level_stats or {}

        # Build coordinate transform (skip if same CRS)
        source_crs = source_layer.crs()
        if (source_crs.isValid() and target_crs.isValid()
                and source_crs.authid() != target_crs.authid()):  # noqa: W503
            self._transform: Optional[QgsCoordinateTransform] = (
                QgsCoordinateTransform(
                    source_crs, target_crs,
                    QgsCoordinateTransformContext()))
        else:
            self._transform = None

        # Detect Level/Layer field name in source
        fields = source_layer.fields()
        self._level_field: Optional[str] = None
        for fname in _LEVEL_FIELD_CANDIDATES:
            if fields.indexFromName(fname) >= 0:
                self._level_field = fname
                break

        # Number of original source fields (used for attribute offset)
        self._n_src_fields = len(source_layer.fields())

    # ── Internal helpers ───────────────────────────────────────────────

    def _get_level(self, feat: QgsFeature) -> str:
        """Extract the level/layer name from a feature."""
        if self._level_field:
            val = feat[self._level_field]
            return str(val) if val not in (None, 'NULL', '') else '0'
        return '0'

    def _encode_str(self, value: str) -> str:
        """Apply OGR text cleaning + optional encoding conversion."""
        value = clean_ogr_text(value)
        if self.encoding_mode != 'UNICODE':
            value = convert_text(value, self.encoding_mode)
        return value

    def _build_out_feature(
        self,
        feat: QgsFeature,
        out_fields: QgsFields,
        level: str,
        kind: str,
    ) -> QgsFeature:
        """
        Construct an output QgsFeature with enriched LVT attributes.

        - Copies all original field values (with encoding conversion).
        - Sets LEVEL, LVT_KIND, CLR_IDX, CLR_HEX, TEXT_STR.
        - Applies coordinate transform to geometry if needed.
        - For TEXT kind: geometry is the centroid/insertion point.
        """
        out_feat = QgsFeature(out_fields)

        # ── Copy original attributes ──────────────────────────────────
        for i in range(self._n_src_fields):
            val = feat[i]
            if isinstance(val, str):
                val = self._encode_str(val)
            out_feat.setAttribute(i, val)

        # ── LVT standard fields ───────────────────────────────────────
        n = self._n_src_fields
        out_feat.setAttribute(n + 0, level)   # LEVEL

        # LVT_KIND: trim to 10 chars (field length limit)
        out_feat.setAttribute(n + 1, kind[:10])   # LVT_KIND

        color_idx, color_hex = extract_color(feat)
        out_feat.setAttribute(n + 2, color_idx if color_idx is not None else -1)
        out_feat.setAttribute(n + 3, color_hex or '')

        # ── Geometry + text ───────────────────────────────────────────
        geom = feat.geometry()
        if kind == GEOM_TEXT:
            # TEXT_STR
            text_val = extract_text(feat) or ''
            text_val = self._encode_str(text_val)
            out_feat.setAttribute(n + 4, text_val[:254])

            # Use centroid as insertion point
            if geom and not geom.isNull():
                centroid = geom.centroid()
                if self._transform:
                    centroid.transform(self._transform)
                out_feat.setGeometry(centroid)
            else:
                out_feat.setGeometry(QgsGeometry())
        else:
            out_feat.setAttribute(n + 4, '')  # TEXT_STR empty for non-text

            if geom and not geom.isNull():
                if self._transform:
                    geom_copy = QgsGeometry(geom)  # don't mutate source
                    geom_copy.transform(self._transform)
                    out_feat.setGeometry(geom_copy)
                else:
                    out_feat.setGeometry(geom)

        return out_feat

    # ── Public API ─────────────────────────────────────────────────────

    def run(self, progress_callback=None) -> list:
        """
        Run the export. Returns list of successfully created SHP paths.

        The export proceeds in two passes:

        1. **Accumulate** all features into ``(level, kind)`` buckets
           while applying filtering and attribute enrichment.
        2. **Write** each non-empty bucket to its own SHP file.

        Args:
            progress_callback: Optional ``callable(percent: int, message: str)``
                called periodically to report progress (0-100).

        Returns:
            List of absolute paths of created SHP files.
        """
        results: list = []
        os.makedirs(self.out_dir, exist_ok=True)

        # Build output field schema (same for all buckets)
        out_fields = build_output_fields(self.source.fields(), 'mixed')

        # ── Pass 1: accumulate features into buckets ──────────────────
        buckets: dict = {}  # (level, kind) → list[QgsFeature]
        total = self.source.featureCount()
        processed = 0

        for feat in self.source.getFeatures():
            processed += 1
            if progress_callback and processed % 200 == 0:
                pct = int(20 + 55 * processed / max(total, 1))
                progress_callback(
                    pct, f'Đọc feature {processed}/{total}...')

            # Determine level
            level = self._get_level(feat)
            if self.filter_levels and level not in self.selected_levels:
                continue

            # Determine geometry kind
            text_val = extract_text(feat)
            geom = feat.geometry()

            if text_val and self.extract_text:
                kind = GEOM_TEXT
            elif geom is None or geom.isNull() or geom.isEmpty():
                kind = GEOM_NONE
            else:
                kind = classify_geom_type(geom.wkbType())

            if kind == GEOM_NONE:
                continue  # skip geometry-less features

            key = (level, kind)
            if key not in buckets:
                buckets[key] = []

            out_feat = self._build_out_feature(feat, out_fields, level, kind)
            buckets[key].append(out_feat)

        # ── Pass 2: write each bucket to a SHP file ───────────────────
        total_buckets = len(buckets)
        for bucket_idx, ((level, kind), features) in enumerate(
                sorted(buckets.items())):
            if not features:
                continue

            if progress_callback:
                pct = int(75 + 20 * bucket_idx / max(total_buckets, 1))
                progress_callback(
                    pct,
                    f'Ghi {level}_{kind} ({len(features)} ĐT)...')

            out_path = make_output_name(level, kind, self.out_dir)
            wkb_type = _KIND_WKB.get(kind, QgsWkbTypes.Type.Unknown)

            writer = QgsVectorFileWriter(
                out_path,
                'UTF-8',
                out_fields,
                wkb_type,
                self.target_crs,
                'ESRI Shapefile',
                ['ENCODING=UTF-8'],
            )
            if writer.hasError() != QgsVectorFileWriter.WriterError.NoError:
                del writer
                continue

            for feat in features:
                writer.addFeature(feat)
            del writer  # flush + close

            # Write companion .cpg file (encoding hint)
            cpg_path = os.path.splitext(out_path)[0] + '.cpg'
            try:
                with open(cpg_path, 'w', encoding='ascii') as cpg:
                    cpg.write('UTF-8')
            except OSError:
                pass  # nosec B110

            if os.path.exists(out_path):
                results.append(out_path)

        if progress_callback:
            progress_callback(100, 'Xuất hoàn tất.')

        return results


# ── High-level convenience function ───────────────────────────────────────

def export_file(
    file_path: str,
    out_dir: str,
    target_crs: QgsCoordinateReferenceSystem,
    selected_levels: list,
    encoding_mode: str = 'UNICODE',
    extract_text: bool = True,
    expand_blocks: bool = True,  # noqa: ARG001 – reserved for future DXF use
    progress_callback=None,
    level_stats: Optional[dict] = None,
) -> list:
    """
    High-level export function: open a CAD file and export selected levels
    to per-level SHP files in ``out_dir``.

    Supported formats (via QGIS OGR provider): ``.dxf``, ``.dgn``.
    For ``.dwg`` files the caller should pre-convert to DXF/GPKG first.

    Args:
        file_path:         Absolute path to the source CAD file.
        out_dir:           Output directory (created if absent).
        target_crs:        Target coordinate reference system.
        selected_levels:   CAD level names to export (empty = all).
        encoding_mode:     Text encoding mode (``'UNICODE'``, ``'TCVN3'``, …).
        extract_text:      If True, create separate Text SHP for text entities.
        expand_blocks:     Reserved — block expansion flag (future).
        progress_callback: Optional ``callable(percent, message)``.
        level_stats:       Pre-scanned level statistics (from _cad_scan).

    Returns:
        List of absolute SHP paths that were successfully written.
    """
    # Primary open attempt
    vlayer = QgsVectorLayer(file_path, 'lvt_export_src', 'ogr')

    # DXF fallback with explicit encoding hint
    if not vlayer.isValid():
        vlayer = QgsVectorLayer(
            f'{file_path}|option:DXF_ENCODING=UTF-8',
            'lvt_export_src', 'ogr')

    if not vlayer.isValid():
        return []

    exporter = LevelExporter(
        source_layer=vlayer,
        out_dir=out_dir,
        target_crs=target_crs,
        selected_levels=selected_levels,
        encoding_mode=encoding_mode,
        extract_text=extract_text,
        level_stats=level_stats,
    )
    results = exporter.run(progress_callback=progress_callback)
    del vlayer
    return results
