"""
Recipe: detect open renewal opportunities per Account.
"""

from __future__ import annotations

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

from g_gremlin import sfdc


@dataclass
class DetectOpenRenewalsResult:
    accounts_queried: int
    renewals_found: int
    ambiguous: int
    rows_written: int


def detect_open_renewals(
    input_path: Path,
    id_column: str,
    renewal_column: str,
    renewal_id_column: str,
    output_path: Path,
    renewal_types: Sequence[str],
    closed_stages: Sequence[str],
    org_alias: Optional[str] = None,
    chunk_size: int = 150,
) -> DetectOpenRenewalsResult:
    """
    Flag accounts that have open renewal opportunities in Salesforce.
    """
    rows, headers = _read_csv(input_path)
    if id_column not in headers:
        raise ValueError(f"ID column '{id_column}' not found in {input_path}.")

    renewal_types_unique = _unique_preserve(renewal_types)
    closed_stages_unique = _unique_preserve(closed_stages)
    if not renewal_types_unique:
        raise ValueError("At least one renewal Type value is required.")
    if not closed_stages_unique:
        raise ValueError("At least one closed StageName value is required.")

    raw_ids: Dict[str, str] = {}
    for row in rows:
        raw = _clean_id(row.get(id_column, ""))
        if not raw:
            continue
        match_key = _match_key(raw)
        if match_key and match_key not in raw_ids:
            raw_ids[match_key] = raw

    if not raw_ids:
        raise ValueError(f"No IDs found in column '{id_column}'.")

    results: Dict[str, List[Dict[str, str]]] = {}
    for chunk in _chunks(list(raw_ids.values()), chunk_size):
        soql = _build_soql(chunk, renewal_types_unique, closed_stages_unique)
        chunk_rows = sfdc.run_soql(soql, org_alias=org_alias)
        for record in chunk_rows:
            acct_key = _match_key(record.get("AccountId", ""))
            if not acct_key:
                continue
            results.setdefault(acct_key, []).append(record)

    renewal_flags = 0
    ambiguous = 0
    output_headers = list(headers)
    if renewal_column not in output_headers:
        output_headers.append(renewal_column)
    if renewal_id_column not in output_headers:
        output_headers.append(renewal_id_column)

    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 row in rows:
            output_row = dict(row)
            raw_id = _clean_id(row.get(id_column, ""))
            if not raw_id:
                output_row[renewal_column] = ""
                output_row[renewal_id_column] = ""
                writer.writerow(output_row)
                continue

            acct_key = _match_key(raw_id)
            candidates = results.get(acct_key, [])
            if candidates:
                renewal_flags += 1
                if len(candidates) > 1:
                    ambiguous += 1
                chosen = _pick_best_renewal(candidates)
                output_row[renewal_column] = "Yes"
                output_row[renewal_id_column] = chosen or ""
            else:
                output_row[renewal_column] = "No"
                output_row[renewal_id_column] = ""
            writer.writerow(output_row)

    return DetectOpenRenewalsResult(
        accounts_queried=len(raw_ids),
        renewals_found=renewal_flags,
        ambiguous=ambiguous,
        rows_written=len(rows),
    )


def _build_soql(ids: Sequence[str], renewal_types: Sequence[str], closed_stages: Sequence[str]) -> str:
    id_list = _format_in_clause(ids)
    renewal_list = _format_in_clause(renewal_types)
    closed_list = _format_in_clause(closed_stages)
    return (
        "SELECT AccountId, Id, Name, StageName, Type, CloseDate "
        "FROM Opportunity "
        f"WHERE AccountId IN ({id_list}) "
        f"AND Type IN ({renewal_list}) "
        f"AND StageName NOT IN ({closed_list})"
    )


def _pick_best_renewal(candidates: List[Dict[str, str]]) -> Optional[str]:
    """
    Choose the candidate with the earliest CloseDate; if no dates, pick the lexicographically
    last StageName as a simple proxy for a later-stage opportunity.
    """
    with_dates: List[Tuple[date, Dict[str, str]]] = []
    without_dates: List[Dict[str, str]] = []
    for record in candidates:
        close_date_raw = (record.get("CloseDate") or "").strip()
        parsed = _parse_date(close_date_raw)
        if parsed:
            with_dates.append((parsed, record))
        else:
            without_dates.append(record)

    if with_dates:
        with_dates.sort(key=lambda pair: pair[0])
        return (with_dates[0][1].get("Id") or "").strip()

    if not without_dates:
        return None

    without_dates.sort(key=lambda rec: (rec.get("StageName") or "").strip())
    return (without_dates[-1].get("Id") or "").strip()


def _parse_date(raw: str) -> Optional[date]:
    if not raw:
        return None
    try:
        return datetime.strptime(raw, "%Y-%m-%d").date()
    except ValueError:
        return None


def _read_csv(path: Path) -> Tuple[List[Dict[str, str]], List[str]]:
    with path.open("r", newline="", encoding="utf-8-sig") as handle:
        reader = csv.DictReader(handle)
        if reader.fieldnames is None:
            raise ValueError(f"CSV {path} is missing a header row.")
        rows = [dict(row) for row in reader]
        return rows, list(reader.fieldnames)


def _clean_id(value: str) -> str:
    return str(value).strip() if value is not None else ""


def _match_key(value: str) -> str:
    return _clean_id(value)[:15].upper()


def _chunks(items: List[str], size: int) -> Iterable[List[str]]:
    for i in range(0, len(items), size):
        yield items[i : i + size]


def _unique_preserve(values: Sequence[str]) -> List[str]:
    return list(dict.fromkeys([v.strip() for v in values if v and v.strip()]))


def _escape_soql_literal(value: str) -> str:
    return value.replace("\\", "\\\\").replace("'", "\\'")


def _format_in_clause(values: Sequence[str]) -> str:
    escaped = [_escape_soql_literal(v) for v in values]
    return ", ".join(f"'{v}'" for v in escaped)
