"""Search across workbook for values using Polars, with openpyxl fallback for formulas."""

import time
from pathlib import Path
from typing import Dict, List, Optional

import typer

from agent_xlsx.adapters.polars_adapter import search_values
from agent_xlsx.cli import app
from agent_xlsx.formatters.json_formatter import output_spreadsheet_data
from agent_xlsx.utils.constants import MAX_SEARCH_LIMIT, MAX_SEARCH_RESULTS
from agent_xlsx.utils.errors import InvalidRegexError, SheetNotFoundError, handle_error
from agent_xlsx.utils.validation import file_size_human, parse_range, validate_file


@app.command()
@handle_error
def search(
    file: str = typer.Argument(..., help="Path to the Excel file"),
    query: str = typer.Argument(..., help="Search term or pattern"),
    regex: bool = typer.Option(False, "--regex", "-r", help="Treat query as regex pattern"),
    ignore_case: bool = typer.Option(False, "--ignore-case", "-i", help="Case-insensitive search"),
    sheet: Optional[str] = typer.Option(None, "--sheet", "-s", help="Search in specific sheet"),
    columns: Optional[str] = typer.Option(
        None,
        "--columns",
        "-c",
        help="Columns to search: letters (A,B,C) or header names (comma-separated)",
    ),
    limit: int = typer.Option(
        MAX_SEARCH_RESULTS,
        "--limit",
        "-l",
        help=f"Max results to return (default: {MAX_SEARCH_RESULTS}, max: {MAX_SEARCH_LIMIT})",
    ),
    range_: Optional[str] = typer.Option(
        None,
        "--range",
        help="Restrict search to range (e.g. 'A1:D100' or 'Sheet!A1:D100')",
    ),
    in_formulas: bool = typer.Option(
        False, "--in-formulas", help="Search in formula strings (uses openpyxl, slower)"
    ),
    no_header: bool = typer.Option(
        False,
        "--no-header",
        help="Treat row 1 as data, use column letters. "
        "Use for non-tabular sheets like P&L reports.",
    ),
) -> None:
    """Search for values across the workbook.

    Uses Polars for fast value searching by default.
    Speed scales with file size; check file_size_human in the output.
    Use --in-formulas to search formula strings via openpyxl.
    """
    path = validate_file(file)

    # Cap limit to safety ceiling
    effective_limit = min(limit, MAX_SEARCH_LIMIT)

    # Parse range if provided
    range_spec = None
    if range_:
        range_spec = parse_range(range_)
        # If range includes a sheet name, it overrides --sheet
        if range_spec["sheet"]:
            sheet = range_spec["sheet"]

    if regex:
        import re

        try:
            re.compile(query)
        except re.error as e:
            raise InvalidRegexError(query, str(e))

    start = time.perf_counter()

    if in_formulas:
        matches = _search_formulas(
            path,
            query,
            sheet,
            regex,
            ignore_case,
            columns=columns,
            limit=effective_limit,
            range_spec=range_spec,
        )
    else:
        try:
            matches = search_values(
                filepath=path,
                query=query,
                sheet_name=sheet,
                regex=regex,
                ignore_case=ignore_case,
                no_header=no_header,
                columns=columns,
                limit=effective_limit,
                range_spec=range_spec,
            )
        except Exception as e:
            # Polars uses a Rust regex engine that rejects some Python-valid
            # patterns (e.g. backreferences, lookaheads). Surface these as
            # structured errors rather than raw tracebacks.
            if regex and "regex" in str(e).lower():
                reason = str(e).strip().rsplit("\n", 1)[-1].strip()
                raise InvalidRegexError(query, reason) from None
            raise

    elapsed_ms = round((time.perf_counter() - start) * 1000, 1)

    result = {
        "query": query,
        "file_size_human": file_size_human(path),
        "match_count": len(matches),
        "matches": matches,
        "truncated": len(matches) >= effective_limit,
        "search_time_ms": elapsed_ms,
    }

    output_spreadsheet_data(result)


def _search_formulas(
    path: Path,
    query: str,
    sheet_name: Optional[str],
    regex: bool,
    ignore_case: bool,
    columns: Optional[str] = None,
    limit: int = MAX_SEARCH_RESULTS,
    range_spec: Optional[Dict] = None,
) -> List[Dict]:
    """Search formula strings via openpyxl (slower path)."""
    import re

    from openpyxl import load_workbook
    from openpyxl.utils import get_column_letter

    wb = load_workbook(str(path), read_only=True, data_only=False)

    if sheet_name:
        if sheet_name not in wb.sheetnames:
            raise SheetNotFoundError(sheet_name, wb.sheetnames)
        target_sheets = [sheet_name]
    else:
        target_sheets = wb.sheetnames

    flags = re.IGNORECASE if ignore_case else 0
    if regex:
        pattern = re.compile(query, flags)
    else:
        search_term = query.lower() if ignore_case else query

    # Resolve column filter to a set of allowed column letters
    allowed_col_letters: set[str] | None = None
    if columns:
        from agent_xlsx.utils.validation import resolve_column_letters

        # Read row-1 headers for header-name resolution (always, even with --range)
        headers = None
        first_ws = wb[target_sheets[0]]
        first_row = next(first_ws.iter_rows(min_row=1, max_row=1, values_only=True), None)
        if first_row:
            from agent_xlsx.utils.validation import index_to_col_letter

            headers = [
                str(v) if v is not None else index_to_col_letter(i) for i, v in enumerate(first_row)
            ]
        allowed_col_letters = resolve_column_letters(columns, headers)

    # Pre-parse range bounds for openpyxl iter_rows
    iter_kwargs: dict = {}
    if range_spec:
        from agent_xlsx.utils.validation import col_letter_to_index

        start_str = range_spec["start"]
        start_col_str = "".join(c for c in start_str if c.isalpha())
        start_row_num = int("".join(c for c in start_str if c.isdigit()))
        iter_kwargs["min_row"] = start_row_num
        iter_kwargs["min_col"] = col_letter_to_index(start_col_str) + 1  # openpyxl 1-based

        if range_spec.get("end"):
            end_str = range_spec["end"]
            end_col_str = "".join(c for c in end_str if c.isalpha())
            end_row_num = int("".join(c for c in end_str if c.isdigit()))
            iter_kwargs["max_row"] = end_row_num
            iter_kwargs["max_col"] = col_letter_to_index(end_col_str) + 1
        else:
            iter_kwargs["max_row"] = start_row_num
            iter_kwargs["max_col"] = iter_kwargs["min_col"]

    matches: list[dict] = []

    for ws_name in target_sheets:
        ws = wb[ws_name]
        for row in ws.iter_rows(values_only=False, **iter_kwargs):
            for cell in row:
                if not (cell.value and isinstance(cell.value, str) and cell.value.startswith("=")):
                    continue

                # Column filter: skip cells not in allowed columns
                cell_col_letter = get_column_letter(cell.column)
                if allowed_col_letters and cell_col_letter not in allowed_col_letters:
                    continue

                formula = cell.value
                matched = False

                if regex:
                    matched = bool(pattern.search(formula))
                elif ignore_case:
                    matched = search_term in formula.lower()
                else:
                    matched = query in formula

                if matched:
                    matches.append(
                        {
                            "sheet": ws_name,
                            "cell": cell.coordinate,
                            "formula": formula,
                        }
                    )
                    if len(matches) >= limit:
                        wb.close()
                        return matches

    wb.close()
    return matches
