"""
Open-Ended Range Validator.

This validator detects unbounded/unstable A1 notations in verification_field_ranges
that can cause flaky verification due to sensitivity to trailing empties and layout changes.

Goal:
Identify open-ended range patterns that should be bounded for stable verification:
- Whole columns: A:A, A:B, C:Z
- Whole rows: 3:3, 10:12  
- Half-bounded columns: A1:A, A1:Z (missing terminating row number)

Why:
Open ranges are sensitive to trailing empties; layout changes create spurious diffs,
making verification unstable. This validator helps identify and suggest fixes.
"""

from __future__ import annotations
import re
from typing import List, Dict, Any, Tuple, Union
import pandas as pd

from urarovite.validators.base import BaseValidator
from urarovite.core.exceptions import ValidationError
from urarovite.utils.sheets import (
    extract_sheet_id,
    split_segments,
    extract_sheet_and_range,
    col_index_to_letter,
    strip_outer_single_quotes,
    get_sheet_values,
)


class OpenEndedRangesValidator(BaseValidator):
    """Validator that detects open-ended ranges in verification field ranges."""
    
    def __init__(self) -> None:
        super().__init__(
            validator_id="open_ended_ranges",
            name="Open-Ended Ranges Detection",
            description=(
                "Detects unbounded A1 notations in verification ranges that "
                "can cause flaky verification (whole columns, rows, half-bounded ranges)"
            ),
        )
    
    # Regular expression patterns for range detection
    COL_RE = r"[A-Z]+"
    ROW_RE = r"[0-9]+"
    
    WHOLE_COL_PATTERN = re.compile(rf"^{COL_RE}:{COL_RE}$")  # A:A, A:B
    WHOLE_ROW_PATTERN = re.compile(rf"^{ROW_RE}:{ROW_RE}$")  # 3:3, 3:10
    HALF_BOUNDED_COL_PATTERN = re.compile(rf"^{COL_RE}{ROW_RE}:{COL_RE}$")  # A1:A, B10:C
    
    DEFAULT_TAB_SCAN_RANGE = "A1:ZZ1000"
    
    def _is_open_ended(self, range_part: str) -> Tuple[str | None, bool]:
        """
        Classify range_part and return (reason, is_open_ended).
        
        We consider fully bounded if both sides have column+row tokens (e.g. A1:B20)
        or single-cell references (A1). Everything else in patterns listed is open-ended.
        """
        if not range_part:
            return None, False
        
        # Single cell like A1 or B20
        if re.fullmatch(rf"{self.COL_RE}{self.ROW_RE}", range_part):
            return None, False
        
        # Whole column or multi-column entire columns
        if self.WHOLE_COL_PATTERN.match(range_part):
            return "whole_column", True
        
        # Whole row(s)
        if self.WHOLE_ROW_PATTERN.match(range_part):
            return "whole_row", True
        
        # Half-bounded column (A1:A or A1:Z) -> second side missing row digits
        if ":" in range_part:
            left, right = range_part.split(":", 1)
            left_col_row = re.fullmatch(rf"{self.COL_RE}{self.ROW_RE}", left) is not None
            right_col_only = re.fullmatch(rf"{self.COL_RE}", right) is not None
            # Fully bounded check: both sides col+row
            both_col_row = (
                left_col_row and 
                re.fullmatch(rf"{self.COL_RE}{self.ROW_RE}", right) is not None
            )
            if both_col_row:
                return None, False  # bounded
            if left_col_row and right_col_only:
                return "half_bounded_column", True
            # If left is col only and right col only (A:B) -> treat as whole columns
            if (re.fullmatch(rf"{self.COL_RE}", left) and 
                re.fullmatch(rf"{self.COL_RE}", right)):
                return "whole_column", True
            # If left is row only and right row only -> whole row(s)
            if (re.fullmatch(rf"{self.ROW_RE}", left) and 
                re.fullmatch(rf"{self.ROW_RE}", right)):
                return "whole_row", True
        
        return None, False
    
    def _suggest_data_bound(
        self, sheet: str, range_part: str, reason: str, rows: int, cols: int
    ) -> str:
        """Generate a bounded range suggestion based on actual data bounds."""
        sheet_prefix = f"{sheet}!" if sheet else ""
        if rows <= 0 or cols <= 0:
            return f"{sheet_prefix}{range_part}"  # fallback
        
        last_col_letter = col_index_to_letter(cols - 1)
        
        if reason == "whole_column":
            parts = range_part.split(":")
            start_col = parts[0]
            end_col = parts[-1]
            return f"{sheet_prefix}{start_col}1:{end_col}{rows}"
        elif reason == "half_bounded_column":
            left, right = range_part.split(":", 1)
            # left like A1 or B10; keep starting row as-is
            return f"{sheet_prefix}{left}:{right}{rows}"
        elif reason == "whole_row":
            rows_part = range_part.split(":")
            start_row = rows_part[0]
            end_row = rows_part[-1]
            return f"{sheet_prefix}A{start_row}:{last_col_letter}{end_row}"
        
        return f"{sheet_prefix}{range_part}"
    
    def _get_tab_bounds(self, spreadsheet_id: str | None, tab: str) -> Tuple[int, int]:
        """
        Return (used_rows, used_cols) for a tab using OAuth authentication.
        
        used_rows: highest 1-based row index containing any non-empty cell (0 if none)
        used_cols: highest 1-based column index containing any non-empty cell (0 if none)
        Returns (0,0) on failure or if spreadsheet_id missing.
        """
        if not spreadsheet_id:
            return 0, 0
        
        # Clean tab name
        clean_tab = strip_outer_single_quotes(tab) if "'" in tab else tab
        
        # Try to get a reasonable range to scan for data bounds
        range_name = f"'{clean_tab}'!{self.DEFAULT_TAB_SCAN_RANGE}"
        
        try:
            result = get_sheet_values(spreadsheet_id, range_name)
            if not result["success"]:
                return 0, 0
            
            return result["rows"], result["cols"]
        
        except Exception:
            return 0, 0
    
    def validate(
        self, sheets_service: Any, sheet_id: str, mode: str, **kwargs: Any
    ) -> Dict[str, Any]:
        """
        Execute open-ended range detection.
        
        Args:
            sheets_service: Google Sheets API service instance
            sheet_id: Not used for this validator (uses URLs from row data)
            mode: Either "fix" (not applicable) or "flag" (report only)
            **kwargs: Must contain 'row' with pandas Series or dict containing the data
            
        Returns:
            Dict with validation results
        """
        # Extract parameters
        row = kwargs.get("row")
        if row is None:
            raise ValidationError("Row data is required for this validator")
        
        field = kwargs.get("field", "verification_field_ranges")
        input_col = kwargs.get("input_col", "input_sheet_url")
        output_col = kwargs.get("output_col", "example_output_sheet_url")
        
        # Extract data from row
        ranges_str = row.get(field, "")
        segments = split_segments(ranges_str)
        open_ended: List[Dict[str, Any]] = []
        
        input_sid = extract_sheet_id(row.get(input_col))
        output_sid = extract_sheet_id(row.get(output_col))
        
        # Cache tab bounds to avoid repeated API calls
        cache_input: Dict[str, Tuple[int, int]] = {}
        cache_output: Dict[str, Tuple[int, int]] = {}
        
        for seg in segments:
            sheet, range_part = extract_sheet_and_range(seg)
            if not range_part:
                continue  # whole-sheet reference
            
            reason, flagged = self._is_open_ended(range_part)
            if not (flagged and reason):
                continue
            
            tab_clean = sheet.strip()
            if tab_clean.startswith("'") and tab_clean.endswith("'"):
                tab_clean = strip_outer_single_quotes(tab_clean)
            
            # Get bounds for both input and output sheets
            if tab_clean not in cache_input:
                cache_input[tab_clean] = self._get_tab_bounds(input_sid, tab_clean)
            if tab_clean not in cache_output:
                cache_output[tab_clean] = self._get_tab_bounds(output_sid, tab_clean)
            
            in_rows, in_cols = cache_input[tab_clean]
            out_rows, out_cols = cache_output[tab_clean]
            
            # Determine best bounds to use for suggestion
            candidates_rows = [v for v in (in_rows, out_rows) if v > 0]
            candidates_cols = [v for v in (in_cols, out_cols) if v > 0]
            
            used_rows = min(candidates_rows) if candidates_rows else 0
            used_cols = min(candidates_cols) if candidates_cols else 0
            
            # Generate suggestion based on available data
            if used_rows > 0 and used_cols > 0:
                suggestion = self._suggest_data_bound(
                    sheet, range_part, reason, used_rows, used_cols
                )
                suggestion_source = "data"
            elif used_rows > 0 and reason in ("whole_column", "half_bounded_column"):
                # Have row info but not column info
                suggestion = self._suggest_data_bound(
                    sheet, range_part, reason, used_rows, 
                    max(in_cols, out_cols) or 26
                )
                suggestion_source = "partial"
            elif used_cols > 0 and reason == "whole_row":
                suggestion = self._suggest_data_bound(
                    sheet, range_part, reason, 
                    max(in_rows, out_rows) or 100, used_cols
                )
                suggestion_source = "partial"
            else:
                # Fallback: use reasonable defaults
                default_rows = 100
                default_cols = 26
                suggestion = self._suggest_data_bound(
                    sheet, range_part, reason, default_rows, default_cols
                )
                suggestion_source = "default"
            
            entry: Dict[str, Any] = {
                "segment": seg,
                "range_part": range_part,
                "reason": reason,
                "suggested": suggestion,
                "suggestion_source": suggestion_source,
                "tab_bounds": {
                    "input": {"rows": in_rows, "cols": in_cols},
                    "output": {"rows": out_rows, "cols": out_cols},
                    "used_rows": used_rows,
                    "used_cols": used_cols,
                },
            }
            open_ended.append(entry)
        
        return {
            "validator": self.id,
            "ok": len(open_ended) == 0,
            "total_segments": len(segments),
            "open_ended": open_ended,
            "original": ranges_str,
        }


# Convenience function for backward compatibility
def run(
    row: Union[Dict[str, Any], "pd.Series"],
    field: str = "verification_field_ranges",
    input_col: str = "input_sheet_url",
    output_col: str = "example_output_sheet_url",
) -> Dict[str, Any]:
    """
    Execute open-ended range detection.
    This function provides backward compatibility with the original checker4 interface.
    """
    validator = OpenEndedRangesValidator()
    
    try:
        return validator.validate(
            sheets_service=None,  # Not used by this validator
            sheet_id="",  # Not used by this validator
            mode="flag",
            row=row,
            field=field,
            input_col=input_col,
            output_col=output_col,
        )
    except Exception as e:
        # Return error in the expected format
        return {
            "validator": validator.id,
            "ok": False,
            "total_segments": 0,
            "open_ended": [],
            "original": "",
            "error": f"validation_error: {str(e)}",
        }
