"""
Preview Salesforce updates by comparing a CSV to current SFDC values.
"""

from __future__ import annotations

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

from g_gremlin import sfdc as sfdc_helpers


@dataclass
class PreviewSummary:
    total_records: int
    records_with_changes: int
    records_unchanged: int
    records_not_found: int


@dataclass
class FieldChange:
    current: Optional[str]
    new: Optional[str]


@dataclass
class RecordChange:
    id: str
    name: Optional[str]
    fields: Dict[str, FieldChange]


@dataclass
class PreviewResult:
    summary: PreviewSummary
    field_summaries: Dict[str, Dict[str, int]]
    changes: List[RecordChange]
    not_found: List[str]


def preview_push(
    csv_path: Path,
    sobject: str,
    id_column: str,
    fields: Sequence[str],
    output_prefix: Path,
    org_alias: Optional[str] = None,
) -> PreviewResult:
    """
    Compare CSV values to current SFDC values for the given object/fields.
    """
    rows, headers = _read_csv(csv_path)
    if id_column not in headers:
        raise ValueError(f"ID column '{id_column}' not found in {csv_path}.")
    if not fields:
        raise ValueError("At least one field is required for preview-push.")

    field_list = list(dict.fromkeys([f.strip() for f in fields if f.strip()]))
    base_fields = ["Id"]
    if "Name" not in field_list:
        base_fields.append("Name")
    select_fields = base_fields + field_list

    id_map: Dict[str, str] = {}
    for row in rows:
        raw_id = (row.get(id_column) or "").strip()
        if not raw_id:
            continue
        key = _match_key(raw_id)
        if key and key not in id_map:
            id_map[key] = raw_id

    sf_records = _fetch_sfdc_records(list(id_map.values()), sobject, select_fields, org_alias)
    changes: List[RecordChange] = []
    not_found: List[str] = []
    field_summaries: Dict[str, Dict[str, int]] = {field: {"changes": 0} for field in field_list}

    for row in rows:
        raw_id = (row.get(id_column) or "").strip()
        if not raw_id:
            continue
        key = _match_key(raw_id)
        sf_rec = sf_records.get(key)
        if sf_rec is None:
            if key not in not_found:
                not_found.append(raw_id)
            continue

        record_field_changes: Dict[str, FieldChange] = {}
        for field in field_list:
            new_val = _clean_val(row.get(field))
            current_val = _clean_val(sf_rec.get(field))
            if _values_different(current_val, new_val):
                record_field_changes[field] = FieldChange(current=current_val, new=new_val)
                field_summaries[field]["changes"] = field_summaries[field].get("changes", 0) + 1

        if record_field_changes:
            changes.append(
                RecordChange(
                    id=sf_rec.get("Id", raw_id),
                    name=sf_rec.get("Name"),
                    fields=record_field_changes,
                )
            )

    summary = PreviewSummary(
        total_records=len(id_map),
        records_with_changes=len(changes),
        records_unchanged=max(len(id_map) - len(changes) - len(not_found), 0),
        records_not_found=len(not_found),
    )

    result = PreviewResult(
        summary=summary,
        field_summaries=field_summaries,
        changes=changes,
        not_found=not_found,
    )
    _write_outputs(result, output_prefix)
    return result


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 _fetch_sfdc_records(
    ids: List[str], sobject: str, fields: List[str], org_alias: Optional[str]
) -> Dict[str, Dict[str, str]]:
    records: Dict[str, Dict[str, str]] = {}
    if not ids:
        return records
    chunk_size = 150
    select_clause = ", ".join(fields)
    for i in range(0, len(ids), chunk_size):
        chunk = ids[i : i + chunk_size]
        soql_ids = ", ".join(f"'{_escape(val)}'" for val in chunk)
        soql = f"SELECT {select_clause} FROM {sobject} WHERE Id IN ({soql_ids})"
        results = sfdc_helpers.run_soql(soql, org_alias=org_alias)
        for rec in results:
            sf_id = rec.get("Id", "")
            key = _match_key(sf_id)
            if key:
                records[key] = rec
    return records


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


def _match_key(value: str) -> str:
    return (value or "").strip()[:15].upper()


def _clean_val(value: Optional[str]) -> Optional[str]:
    if value is None:
        return None
    text = str(value).strip()
    return text if text != "" else None


def _values_different(current: Optional[str], new: Optional[str]) -> bool:
    return (current or "") != (new or "")


def _write_outputs(result: PreviewResult, output_prefix: Path) -> None:
    base = output_prefix
    if base.suffix:
        base = base.with_suffix("")
    json_path = base.with_suffix(".json")
    md_path = base.with_suffix(".md")
    json_payload = {
        "summary": {
            "total_records": result.summary.total_records,
            "records_with_changes": result.summary.records_with_changes,
            "records_unchanged": result.summary.records_unchanged,
            "records_not_found": result.summary.records_not_found,
        },
        "field_summaries": result.field_summaries,
        "changes": [
            {
                "id": rc.id,
                "name": rc.name,
                "fields": {k: {"current": v.current, "new": v.new} for k, v in rc.fields.items()},
            }
            for rc in result.changes
        ],
        "not_found": result.not_found,
    }
    json_path.parent.mkdir(parents=True, exist_ok=True)
    json_path.write_text(json.dumps(json_payload, indent=2), encoding="utf-8")
    md_path.write_text(_render_markdown(result), encoding="utf-8")


def _render_markdown(result: PreviewResult) -> str:
    lines: List[str] = []
    summary = result.summary
    lines.append("# SFDC Preview")
    lines.append("")
    lines.append(
        f"**Summary:** {summary.records_with_changes} of {summary.total_records} record(s) would be updated "
        f"(unchanged: {summary.records_unchanged}, not found: {summary.records_not_found})"
    )
    lines.append("")
    lines.append("## Field Changes")
    lines.append("")
    lines.append("| Field | Updates |")
    lines.append("|-------|---------|")
    for field, stats in result.field_summaries.items():
        lines.append(f"| {field} | {stats.get('changes', 0)} |")

    if result.changes:
        lines.append("")
        lines.append("## Detailed Changes")
        lines.append("")
        for rc in result.changes[:20]:  # limit detail
            lines.append(f"### {rc.id} {f'({rc.name})' if rc.name else ''}".strip())
            lines.append("| Field | Current | New |")
            lines.append("|-------|---------|-----|")
            for field, change in rc.fields.items():
                lines.append(f"| {field} | {change.current or ''} | {change.new or ''} |")
            lines.append("")
        if len(result.changes) > 20:
            lines.append(f"... ({len(result.changes) - 20} more records with changes)")

    if result.not_found:
        lines.append("")
        lines.append(f"Records not found in SFDC: {len(result.not_found)}")
        preview = result.not_found[:10]
        lines.append(", ".join(preview))
        if len(result.not_found) > 10:
            lines.append("...")

    return "\n".join(lines)
