"""
Recipe: attach distinct product names per Account from OpportunityLineItems.
"""

from __future__ import annotations

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

from g_gremlin import sfdc


@dataclass
class AttachProductsResult:
    accounts_queried: int
    with_products: int
    rows_written: int


def attach_products(
    input_path: Path,
    id_column: str,
    products_column: str,
    output_path: Path,
    stage: str,
    org_alias: Optional[str] = None,
    chunk_size: int = 150,
) -> AttachProductsResult:
    """
    Attach a comma-separated list of product names per Account based on OpportunityLineItems.
    """
    rows, headers = _read_csv(input_path)
    if id_column not in headers:
        raise ValueError(f"ID column '{id_column}' not found in {input_path}.")

    stage_clean = stage.strip()
    if not stage_clean:
        raise ValueError("Stage value cannot be empty.")

    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}'.")

    products_by_account: Dict[str, Set[str]] = {}
    for chunk in _chunks(list(raw_ids.values()), chunk_size):
        soql = _build_soql(chunk, stage_clean)
        chunk_rows = sfdc.run_soql(soql, org_alias=org_alias)
        for record in chunk_rows:
            acct_key = _match_key(record.get("Opportunity.AccountId", ""))
            product = (record.get("Product2.Name") or "").strip()
            if not acct_key or not product:
                continue
            products_by_account.setdefault(acct_key, set()).add(product)

    output_headers = list(headers)
    if products_column not in output_headers:
        output_headers.append(products_column)

    accounts_with_products = 0
    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[products_column] = ""
                writer.writerow(output_row)
                continue

            acct_key = _match_key(raw_id)
            products = products_by_account.get(acct_key, set())
            if products:
                accounts_with_products += 1
            output_row[products_column] = ", ".join(sorted(products)) if products else ""
            writer.writerow(output_row)

    return AttachProductsResult(
        accounts_queried=len(raw_ids),
        with_products=accounts_with_products,
        rows_written=len(rows),
    )


def _build_soql(ids: Sequence[str], stage: str) -> str:
    id_list = _format_in_clause(ids)
    stage_value = _escape_soql_literal(stage)
    return (
        "SELECT Opportunity.AccountId, Product2.Name "
        "FROM OpportunityLineItem "
        f"WHERE Opportunity.AccountId IN ({id_list}) "
        f"AND Opportunity.StageName = '{stage_value}'"
    )


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 _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)
