from __future__ import annotations

from datetime import datetime
from pathlib import Path
from typing import Optional, Tuple

from pygui.backend.wafer.zwafer_models import Sensor, ZWaferData


# ===== Z-Wafer CSV Parser =====
class ZWaferParser:
    """Parses Z-wafer CSV files (header + sensor layout + data rows)."""

    # ===== Public Parse API =====
    def parse(self, file_path: str) -> Tuple[Optional[ZWaferData], Optional[Path]]:
        """
        Parse a Z-wafer file.

        Returns:
            (ZWaferData, Path) on success
            (None, None) on error (e.g., file not found)
        """
        try:
            path = Path(file_path)
            if not path.exists():
                raise FileNotFoundError(f"File not found: {file_path}")

            with path.open("r", encoding="utf-8") as f:
                return self._process_header(f), path
        except (ValueError, KeyError):
            raise
        except Exception:
            return None, None

    # ===== Header Parsing =====
    def _process_header(self, file_obj) -> ZWaferData:
        """Parse header and sensor layout from open file object.
        Handles two CSV row orderings:
        1. Label/X/Y rows BEFORE 'data' (fixture test format)
        2. 'data' row BEFORE Label/X/Y (some real CSV files)
        The C# parser handles both by collecting sensor layout rows
        regardless of whether 'data' has been seen.
        """
        wafer_data = ZWaferData()
        labels: Optional[list] = None
        x_coords: Optional[list] = None
        y_coords: Optional[list] = None

        for line in file_obj:
            # Strip trailing comma + whitespace
            line = line.rstrip().rstrip(",").strip()
            if not line or line.replace(",", "").strip() == "":
                continue

            parts = [p.strip() for p in line.split(",")]
            first_part = parts[0].lower()

            # Collect sensor layout rows (Label, X (mm), Y (mm))
            # regardless of whether 'data' has been seen.
            # This matches the C# parser behavior.
            if first_part == "label":
                labels = parts[1:]
            elif first_part == "x (mm)":
                x_coords = parts[1:]
            elif first_part == "y (mm)":
                y_coords = parts[1:]
            # Parse metadata (key=value pairs) — only when not a sensor layout row
            elif first_part != "data":
                self._parse_header_line(wafer_data, parts)

            # Detect end of header section
            if first_part == "data":
                wafer_data.csv_headers = labels or []
                self._build_sensor_layout(wafer_data, labels, x_coords, y_coords)
                return wafer_data

        return wafer_data  # Incomplete file — return partial data

    # ===== Data Row Extraction =====
    def parse_data_rows(self, file_path: str) -> list[str]:
        """Return data rows after the 'data' marker, with the leading
        row-index column stripped (each row realigned to `csv_headers`).

        Uses the same line normalization as `_process_header` so the split
        point matches exactly what header parsing used.
        """
        rows: list[str] = []
        seen_data_marker = False
        with Path(file_path).open("r", encoding="utf-8") as f:
            for raw_line in f:
                line = raw_line.rstrip().rstrip(",").strip()
                if not line or line.replace(",", "").strip() == "":
                    continue

                parts = [p.strip() for p in line.split(",")]
                if not seen_data_marker:
                    if parts[0].lower() == "data":
                        seen_data_marker = True
                    continue

                rows.append(",".join(parts[1:]))
        return rows

    # ===== Metadata Parsing =====
    def _parse_header_line(self, wafer_data: ZWaferData, parts: list) -> bool:
        """Parse key=value pairs from header line. Returns True if handled."""
        non_empty_parts = [p for p in parts if p]
        if not non_empty_parts:
            return False

        found_kv = False
        for part in non_empty_parts:
            eq_idx = part.find("=")
            if eq_idx < 0:
                continue

            key = part[:eq_idx].strip()
            value = part[eq_idx + 1 :].strip('=" ')
            found_kv = True

            wafer_data.header[key] = value

            # Extract special fields
            if key.lower() == "acquisition date":
                try:
                    from datetime import datetime as dt

                    wafer_data.date = dt.strptime(value, "%m/%d/%Y")
                except ValueError:
                    wafer_data.date = datetime.min  # Fallback on parse error
            elif key.lower() == "wafer id":
                wafer_data.serial = value

        return found_kv

    # ===== Sensor Layout Parsing =====
    def _build_sensor_layout(
        self,
        wafer_data: ZWaferData,
        labels: Optional[list],
        x_coords: Optional[list],
        y_coords: Optional[list],
    ) -> None:
        """Build sensor list from layout arrays."""
        if not (labels and x_coords and y_coords):
            raise ValueError("Sensor layout section is incomplete or missing.")

        if len(labels) != len(x_coords) or len(labels) != len(y_coords):
            raise ValueError(
                f"Mismatched sensor columns: labels={len(labels)}, "
                f"x={len(x_coords)}, y={len(y_coords)}"
            )

        for i in range(len(labels)):
            try:
                wafer_data.sensors.append(
                    Sensor(label=labels[i], x=float(x_coords[i]), y=float(y_coords[i]))
                )
            except ValueError as e:
                raise ValueError(f"Invalid coordinate at index {i}: {e}")
