from dataclasses import dataclass, field
from typing import List, Optional, Any, Dict


@dataclass
class NormalizationReport:
    """
    Captures metrics, metadata, and warnings during the normalization process.
    """

    # Counters
    total_lines_read: int = 0
    metadata_lines_skipped: int = 0
    trailer_lines_skipped: int = 0
    duplicate_headers_removed: int = 0
    rows_emitted: int = 0

    # Validation values
    row_count_trailer_value: Optional[int] = None

    # Metadata extracted during normalization (e.g. report dates)
    extracted_metadata: Dict[str, Any] = field(default_factory=dict)

    # Issues
    warnings: List[str] = field(default_factory=list)

    def add_warning(self, message: str) -> None:
        """Add a warning message to the report."""
        self.warnings.append(message)

    def increment_total_lines(self) -> None:
        """Increment the total count of lines read from the source."""
        self.total_lines_read += 1

    def increment_metadata_skipped(self) -> None:
        """Increment the count of non-header metadata lines skipped."""
        self.metadata_lines_skipped += 1

    def increment_trailer_skipped(self) -> None:
        """Increment the count of trailer/footer lines skipped."""
        self.trailer_lines_skipped += 1

    def increment_duplicate_headers(self) -> None:
        """Increment the count of repeated header rows removed."""
        self.duplicate_headers_removed += 1

    def increment_rows_emitted(self) -> None:
        """Increment the count of valid data rows emitted by the pipeline."""
        self.rows_emitted += 1

    def set_trailer_row_count(self, count: int) -> None:
        """Set the row count value extracted from a trailer line."""
        self.row_count_trailer_value = count

    def validate_row_count(self, strict: bool = False) -> None:
        """Validate that trailer row count matches rows emitted.

        Args:
            strict: If True, raise ValueError on mismatch. Otherwise, add warning.

        Raises:
            ValueError: If strict=True and counts don't match.
        """
        if self.row_count_trailer_value is None:
            # No trailer row count to validate
            return

        if self.row_count_trailer_value != self.rows_emitted:
            msg = (
                f"Row count mismatch: trailer indicates {self.row_count_trailer_value} "
                f"rows, but {self.rows_emitted} rows were emitted"
            )
            if strict:
                raise ValueError(msg)
            else:
                self.add_warning(msg)


@dataclass
class NormalizationStepConfig:
    """
    Configuration for a single normalization step.
    """

    step: str
    config: Dict[str, Any] = field(default_factory=dict)
    enabled: bool = True


@dataclass
class NormalizationConfig:
    """
    Top-level configuration for the normalization pipeline.
    """

    strict: bool = False
    steps: List[NormalizationStepConfig] = field(default_factory=list)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "NormalizationConfig":
        """
        Factory to create config from a dictionary (e.g., parsed YAML).
        """
        strict = data.get("strict", False)
        raw_steps = data.get("steps", [])

        parsed_steps = []
        for step_data in raw_steps:
            if isinstance(step_data, dict):
                # Validate required 'step' field
                if "step" not in step_data:
                    raise ValueError("Normalization step missing required 'step' name.")

                parsed_steps.append(
                    NormalizationStepConfig(
                        step=step_data["step"],
                        config=step_data.get("config", {}),
                        enabled=step_data.get("enabled", True),
                    )
                )
            else:
                raise ValueError(f"Invalid step definition: {step_data}")

        return cls(strict=strict, steps=parsed_steps)
