"""
Gremlin sheet profiling utilities.

Builds compact, non-PII profiles for full-sheet awareness.
"""

from __future__ import annotations

from collections import Counter
from datetime import datetime, timezone
import hashlib
import json
import re
from typing import Any, List, Optional, Tuple

from pydantic import BaseModel, Field


EMAIL_RE = re.compile(r"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
URL_RE = re.compile(r"^https?://", re.IGNORECASE)
DOMAIN_RE = re.compile(r"^([a-z0-9-]+\\.)+[a-z]{2,}$", re.IGNORECASE)
PHONE_RE = re.compile(r"^\\+?[0-9\\-\\s().]{7,}$")
DATE_RE = re.compile(r"^\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}$")

MAX_PATTERN_SAMPLES = 200
MAX_PROFILE_ROWS = 20000
MAX_PROFILE_COLS = 200
MAX_PROFILE_CELLS = 500000


class ColumnProfile(BaseModel):
    index: int
    header: str
    inferred_type: str
    non_empty_count: int
    unique_count: int
    uniqueness_ratio: float
    top_patterns: List[str] = Field(default_factory=list)
    likely_key: bool = False
    likely_foreign_key: bool = False
    domain_detected: bool = False
    email_detected: bool = False
    phone_detected: bool = False


class DuplicateSignals(BaseModel):
    likely_duplicate_columns: List[str] = Field(default_factory=list)
    estimated_duplicate_rows: int = 0
    duplicate_confidence: float = 0.0


class TableBounds(BaseModel):
    start_row: int
    start_col: int
    end_row: int
    end_col: int


class SheetProfile(BaseModel):
    spreadsheet_id: str
    sheet_name: str
    revision: str
    content_hash: Optional[str] = None
    profiled_at: datetime
    total_rows: int
    total_cols: int
    data_bounds: Tuple[int, int, int, int]
    columns: List[ColumnProfile] = Field(default_factory=list)
    likely_header_row: int = 1
    detected_tables: List[TableBounds] = Field(default_factory=list)
    duplicate_signals: DuplicateSignals = Field(default_factory=DuplicateSignals)
    representative_samples: List[List[str]] = Field(default_factory=list)
    profile_truncated: bool = False


def compute_content_hash(headers: List[Any], sample_rows: List[List[Any]]) -> str:
    """Hash headers + first 100 rows for profile cache keys."""

    def _normalize_value(value: Any) -> str:
        if value is None:
            return ""
        if isinstance(value, float):
            if value.is_integer():
                return str(int(value))
            return format(value, "g")
        return str(value)

    normalized_headers = [_normalize_value(h) for h in (headers or [])]
    normalized_rows: List[List[str]] = []
    for row in (sample_rows or [])[:100]:
        normalized_rows.append([_normalize_value(cell) for cell in row])
    payload = json.dumps(
        {"headers": normalized_headers, "rows": normalized_rows},
        separators=(",", ":"),
        ensure_ascii=True,
    )
    return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]


def column_index_to_letter(idx: int) -> str:
    """Convert 1-based column index to letters (A, B, ...)."""
    if idx < 1:
        return "A"
    letters = ""
    while idx > 0:
        idx, rem = divmod(idx - 1, 26)
        letters = chr(65 + rem) + letters
    return letters


def _normalize_cell(cell: Any) -> str:
    if cell is None:
        return ""
    if isinstance(cell, str):
        return cell.strip()
    return str(cell).strip()


def _classify_value(value: str) -> str:
    if not value:
        return "empty"
    if EMAIL_RE.match(value):
        return "email"
    if URL_RE.match(value):
        return "url"
    if DOMAIN_RE.match(value):
        return "domain"
    if PHONE_RE.match(value):
        digits = re.sub(r"\\D", "", value)
        if len(digits) >= 7:
            return "phone"
    if DATE_RE.match(value):
        return "date"
    try:
        float(value.replace(",", ""))
        return "number"
    except Exception:
        return "text"


def _infer_type(sample_values: List[str]) -> str:
    counts = Counter()
    for val in sample_values:
        counts[_classify_value(val)] += 1
    if not counts:
        return "text"
    counts.pop("empty", None)
    if not counts:
        return "text"
    inferred = counts.most_common(1)[0][0]
    return inferred


def _patternize_value(value: str, inferred_type: str) -> str:
    if not value:
        return ""
    if inferred_type == "email":
        return "xxx@xxx.com"
    if inferred_type in ("domain", "url"):
        return "domain"
    if inferred_type == "phone":
        return "xxx-xxx-xxxx"
    if inferred_type == "number":
        return "###"
    if inferred_type == "date":
        return "yyyy-mm-dd"
    return f"text({len(value)})"


def _compute_column_profiles(headers: List[str], data_rows: List[List[Any]]) -> List[ColumnProfile]:
    total_cols = max(len(headers), max((len(r) for r in data_rows), default=0))
    normalized_headers: List[str] = []
    for idx in range(total_cols):
        header = headers[idx] if idx < len(headers) else ""
        header_str = str(header or "").strip()
        normalized_headers.append(header_str or f"Column {idx + 1}")

    profiles: List[ColumnProfile] = []
    for idx in range(total_cols):
        sample_values: List[str] = []
        unique_values = set()
        non_empty = 0

        for row in data_rows:
            cell = row[idx] if idx < len(row) else ""
            value = _normalize_cell(cell)
            if value:
                non_empty += 1
                if len(sample_values) < MAX_PATTERN_SAMPLES:
                    sample_values.append(value)
                unique_values.add(value.lower())

        inferred = _infer_type(sample_values)
        pattern_counts = Counter()
        for sample in sample_values:
            pattern_counts[_patternize_value(sample, inferred)] += 1
        top_patterns = [p for p, _ in pattern_counts.most_common(3) if p]

        unique_count = len(unique_values)
        uniqueness_ratio = unique_count / non_empty if non_empty else 0.0
        likely_key = uniqueness_ratio >= 0.9 and non_empty >= 10

        profile = ColumnProfile(
            index=idx + 1,
            header=normalized_headers[idx],
            inferred_type=inferred,
            non_empty_count=non_empty,
            unique_count=unique_count,
            uniqueness_ratio=round(uniqueness_ratio, 4),
            top_patterns=top_patterns,
            likely_key=likely_key,
            likely_foreign_key=False,
            domain_detected=inferred in ("domain", "url"),
            email_detected=inferred == "email",
            phone_detected=inferred == "phone",
        )
        profiles.append(profile)
    return profiles


def _compute_duplicate_signals(columns: List[ColumnProfile]) -> DuplicateSignals:
    candidates = []
    for col in columns:
        duplicates = col.non_empty_count - col.unique_count
        if duplicates > 0:
            candidates.append((duplicates, col.header, col.non_empty_count))

    if not candidates:
        return DuplicateSignals()

    candidates.sort(reverse=True)
    top = candidates[:3]
    likely_columns = [c[1] for c in top]
    estimated_duplicates = max(c[0] for c in top)
    confidence = max((c[0] / c[2]) if c[2] else 0 for c in top)
    return DuplicateSignals(
        likely_duplicate_columns=likely_columns,
        estimated_duplicate_rows=int(estimated_duplicates),
        duplicate_confidence=round(confidence, 3),
    )


def _build_representative_samples(
    data_rows: List[List[Any]],
    columns: List[ColumnProfile],
    max_rows: int = 10,
    max_cols: int = 5,
) -> List[List[str]]:
    if not data_rows:
        return []
    row_count = len(data_rows)
    step = max(row_count // max_rows, 1)
    indices = list(range(0, row_count, step))[:max_rows]
    samples: List[List[str]] = []
    for idx in indices:
        row = data_rows[idx]
        sample_row: List[str] = []
        for col_idx in range(min(max_cols, len(columns))):
            cell = row[col_idx] if col_idx < len(row) else ""
            value = _normalize_cell(cell)
            inferred = columns[col_idx].inferred_type if col_idx < len(columns) else "text"
            sample_row.append(_patternize_value(value, inferred))
        samples.append(sample_row)
    return samples


def build_sheet_profile(
    values: List[List[Any]],
    spreadsheet_id: str,
    sheet_name: str,
    revision: str,
    total_rows: int,
    total_cols: int,
    content_hash: Optional[str] = None,
    profile_truncated: bool = False,
) -> SheetProfile:
    headers = values[0] if values else []
    data_rows = values[1:] if len(values) > 1 else []
    if not content_hash:
        content_hash = compute_content_hash(headers, data_rows)
    columns = _compute_column_profiles(headers, data_rows)
    duplicate_signals = _compute_duplicate_signals(columns)
    representative_samples = _build_representative_samples(data_rows, columns)

    bounds = (1, 1, max(total_rows, 1), max(total_cols, 1))
    detected_tables = [TableBounds(start_row=1, start_col=1, end_row=bounds[2], end_col=bounds[3])]

    return SheetProfile(
        spreadsheet_id=spreadsheet_id,
        sheet_name=sheet_name,
        revision=revision,
        content_hash=content_hash,
        profiled_at=datetime.now(timezone.utc),
        total_rows=total_rows,
        total_cols=total_cols,
        data_bounds=bounds,
        columns=columns,
        likely_header_row=1,
        detected_tables=detected_tables,
        duplicate_signals=duplicate_signals,
        representative_samples=representative_samples,
        profile_truncated=profile_truncated,
    )


def fetch_sheet_values(
    service: Any,
    spreadsheet_id: str,
    sheet_name: str,
) -> Tuple[List[List[Any]], int, int, bool]:
    """
    Fetch sheet values with safety caps. Returns (values, total_rows, total_cols, truncated).
    """
    meta = (
        service.spreadsheets()
        .get(spreadsheetId=spreadsheet_id, fields="sheets.properties")
        .execute()
    )
    sheet_props = None
    for sheet in meta.get("sheets", []):
        props = sheet.get("properties", {})
        if props.get("title") == sheet_name:
            sheet_props = props
            break

    grid = (sheet_props or {}).get("gridProperties", {})
    grid_rows = int(grid.get("rowCount") or 0)
    grid_cols = int(grid.get("columnCount") or 0)

    max_rows = min(grid_rows or MAX_PROFILE_ROWS, MAX_PROFILE_ROWS)
    max_cols = min(grid_cols or MAX_PROFILE_COLS, MAX_PROFILE_COLS)
    if max_rows * max_cols > MAX_PROFILE_CELLS:
        max_rows = max(1, MAX_PROFILE_CELLS // max_cols)

    truncated = (grid_rows and grid_rows > max_rows) or (grid_cols and grid_cols > max_cols)

    range_a1 = f"'{sheet_name}'!A1:{column_index_to_letter(max_cols)}{max_rows}"
    values = (
        service.spreadsheets()
        .values()
        .get(
            spreadsheetId=spreadsheet_id,
            range=range_a1,
            valueRenderOption="UNFORMATTED_VALUE",
        )
        .execute()
        .get("values", [])
    )

    actual_rows = len(values)
    actual_cols = max((len(row) for row in values), default=0)
    total_rows = actual_rows or 0
    total_cols = actual_cols or 0
    if (actual_rows == max_rows and grid_rows and grid_rows > max_rows) or (
        actual_cols == max_cols and grid_cols and grid_cols > max_cols
    ):
        truncated = True

    return values, total_rows, total_cols, truncated
