"""
Recipe: build_account_dim from opportunity exports (and optional master list).
"""

from __future__ import annotations

import csv
from collections import Counter
from datetime import datetime, date
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Sequence, Set, Tuple

from difflib import SequenceMatcher

from g_gremlin.normalize import normalize_name


@dataclass
class AccountAggregate:
    sources: Set[str]
    opp_count: int
    name_counts: Counter
    has_closed_won: bool
    latest_close_date: Optional[date]
    latest_end_date: Optional[date]
    arr_total: float
    arr_closed_won: float


@dataclass
class BuildAccountDimResult:
    accounts: int
    sources: List[str]


def build_account_dim(
    opp_sources: List[Path],
    key_column: str,
    name_column: str,
    output_path: Path,
    master_list: Optional[Path] = None,
    master_name_col: Optional[str] = None,
    match_threshold: float = 0.85,
    stage_column: Optional[str] = "StageName",
    close_date_column: Optional[str] = "CloseDate",
    end_date_column: Optional[str] = None,
    amount_column: Optional[str] = "Amount",
    closed_won_values: Optional[Sequence[str]] = None,
) -> BuildAccountDimResult:
    """
    Build a simple account dimension from opp exports.
    """
    closed_won_set = {
        val.strip().lower()
        for val in (closed_won_values if closed_won_values is not None else ["Closed Won", "7 - Closed Won"])
        if val and val.strip()
    }
    aggregates: Dict[str, AccountAggregate] = {}
    source_tags: Set[str] = set()

    for src in opp_sources:
        tag = src.stem
        source_tags.add(tag)
        with src.open("r", newline="", encoding="utf-8-sig") as handle:
            reader = csv.DictReader(handle)
            if reader.fieldnames is None:
                raise ValueError(f"CSV {src} is missing a header row.")
            for row in reader:
                key = (row.get(key_column) or "").strip()
                if not key:
                    continue
                name_val = (row.get(name_column) or "").strip()
                agg = aggregates.get(key)
                if agg is None:
                    agg = AccountAggregate(
                        sources=set(),
                        opp_count=0,
                        name_counts=Counter(),
                        has_closed_won=False,
                        latest_close_date=None,
                        latest_end_date=None,
                        arr_total=0.0,
                        arr_closed_won=0.0,
                    )
                    aggregates[key] = agg
                agg.sources.add(tag)
                agg.opp_count += 1
                if name_val:
                    agg.name_counts[name_val] += 1

                stage_val = (row.get(stage_column) or "").strip().lower() if stage_column else ""
                is_closed_won = stage_val in closed_won_set if stage_val else False
                if is_closed_won:
                    agg.has_closed_won = True

                if close_date_column:
                    close_dt = _parse_date_value(row.get(close_date_column))
                    if close_dt and (agg.latest_close_date is None or close_dt > agg.latest_close_date):
                        agg.latest_close_date = close_dt

                if end_date_column:
                    end_dt = _parse_date_value(row.get(end_date_column))
                    if end_dt and (agg.latest_end_date is None or end_dt > agg.latest_end_date):
                        agg.latest_end_date = end_dt

                if amount_column:
                    amount_val = _parse_amount(row.get(amount_column))
                    if amount_val is not None:
                        agg.arr_total += amount_val
                        if is_closed_won:
                            agg.arr_closed_won += amount_val

    master_names: List[Tuple[str, str]] = []
    if master_list:
        if not master_name_col:
            raise ValueError("master-name-col is required when providing a master-list.")
        master_names = _load_master_names(master_list, master_name_col)

    base_headers = [key_column, "Canonical_Account_Name", "Source", "Opp_Count"]
    if master_list:
        base_headers.extend(["Original_Master_Name", "In_Original_Master"])
    metrics_headers = [
        "Has_ClosedWon",
        "Latest_Close_Date",
        "Latest_End_Date",
        "ARR_Estimate",
        "Status",
        "Needs_Manual_Review",
    ]
    output_headers = base_headers + metrics_headers

    output_path.parent.mkdir(parents=True, exist_ok=True)
    with output_path.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=output_headers)
        writer.writeheader()
        for account_id, agg in aggregates.items():
            canonical_name = _pick_canonical_name(agg.name_counts)
            row = {
                key_column: account_id,
                "Canonical_Account_Name": canonical_name,
                "Source": ",".join(sorted(agg.sources)),
                "Opp_Count": str(agg.opp_count),
            }
            if master_list:
                best_master = _best_master_match(canonical_name, master_names, match_threshold)
                row["Original_Master_Name"] = best_master if best_master else ""
                row["In_Original_Master"] = "Y" if best_master else "N"
            arr_estimate = agg.arr_closed_won if agg.arr_closed_won > 0 else agg.arr_total
            status = _status_from_agg(agg)
            manual_review = _needs_manual_review(
                status=status,
                in_master=row.get("In_Original_Master") if master_list else None,
            )
            row["Has_ClosedWon"] = "Y" if agg.has_closed_won else "N"
            row["Latest_Close_Date"] = agg.latest_close_date.isoformat() if agg.latest_close_date else ""
            row["Latest_End_Date"] = agg.latest_end_date.isoformat() if agg.latest_end_date else ""
            row["ARR_Estimate"] = f"{arr_estimate:.2f}" if arr_estimate else "0"
            row["Status"] = status
            row["Needs_Manual_Review"] = manual_review
            writer.writerow(row)

    return BuildAccountDimResult(accounts=len(aggregates), sources=sorted(source_tags))


def _pick_canonical_name(name_counts: Counter) -> str:
    if not name_counts:
        return ""
    most_common = name_counts.most_common()
    return most_common[0][0]


def _load_master_names(master_list: Path, master_name_col: str) -> List[Tuple[str, str]]:
    with master_list.open("r", newline="", encoding="utf-8-sig") as handle:
        reader = csv.DictReader(handle)
        if reader.fieldnames is None:
            raise ValueError(f"Master list {master_list} is missing a header row.")
        names: List[Tuple[str, str]] = []
        for row in reader:
            raw = (row.get(master_name_col) or "").strip()
            if not raw:
                continue
            names.append((normalize_name(raw), raw))
        return names


def _best_master_match(name: str, master_names: List[Tuple[str, str]], threshold: float) -> Optional[str]:
    if not name:
        return None
    norm_name = normalize_name(name)
    best_score = 0.0
    best_value: Optional[str] = None
    for norm_master, raw_master in master_names:
        if not norm_master:
            continue
        score = SequenceMatcher(None, norm_name, norm_master).ratio()
        if score > best_score:
            best_score = score
            best_value = raw_master
    if best_score >= threshold:
        return best_value
    return None


def _parse_date_value(raw: Optional[str]) -> Optional[date]:
    if not raw:
        return None
    text = str(raw).strip()
    if not text:
        return None
    for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%m/%d/%Y"):
        try:
            return datetime.strptime(text, fmt).date()
        except ValueError:
            continue
    try:
        return datetime.fromisoformat(text.replace("Z", "+00:00")).date()
    except ValueError:
        return None


def _parse_amount(raw: Optional[str]) -> Optional[float]:
    if raw is None:
        return None
    text = str(raw).strip().replace("$", "").replace(",", "")
    if not text:
        return None
    try:
        return float(text)
    except ValueError:
        return None


def _status_from_agg(agg: AccountAggregate) -> str:
    if agg.opp_count == 0:
        return "Missing_Opps"
    if agg.has_closed_won:
        return "Active"
    return "Unknown"


def _needs_manual_review(status: str, in_master: Optional[str]) -> str:
    if status == "Unknown":
        return "Y"
    if in_master is not None and in_master != "Y":
        return "Y"
    return "N"
