#!/usr/bin/env python3
"""Deterministic, dependency-free workflow automation proof.

The proof models a single trigger (an enquiry JSON payload) and one queue
destination (a CSV report).  It validates and normalises records, deduplicates
by normalised email, preserves records when the destination is unavailable,
and recovers them idempotently when the destination returns.
"""

from __future__ import annotations

import argparse
import csv
import io
import json
import os
import re
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable, Mapping


EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
CSV_FIELDS = [
    "enquiry_id",
    "received_at",
    "name",
    "email",
    "company",
    "process",
    "message",
    "status",
    "attempts",
]
REQUIRED_TEXT_FIELDS = ("name", "email", "company", "process", "message")


class DestinationUnavailable(RuntimeError):
    """Raised when the report destination cannot accept a write."""


def utc_now() -> str:
    """Return a compact UTC timestamp suitable for evidence files."""

    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace(
        "+00:00", "Z"
    )


def _normalise_text(value: Any) -> str:
    return " ".join(str(value).strip().split()) if value is not None else ""


def _atomic_write_text(path: Path, content: str) -> None:
    """Write a file through a same-directory temporary file and replace."""

    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    fd, temporary_name = tempfile.mkstemp(
        prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent)
    )
    temporary_path = Path(temporary_name)
    try:
        with os.fdopen(fd, "w", encoding="utf-8", newline="") as handle:
            handle.write(content)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary_path, path)
    finally:
        if temporary_path.exists():
            temporary_path.unlink()


def _atomic_write_json(path: Path, payload: Mapping[str, Any]) -> None:
    _atomic_write_text(
        Path(path), json.dumps(payload, indent=2, sort_keys=True) + "\n"
    )


def _atomic_write_csv(path: Path, rows: Iterable[Mapping[str, Any]]) -> None:
    output = io.StringIO(newline="")
    writer = csv.DictWriter(output, fieldnames=CSV_FIELDS, extrasaction="ignore")
    writer.writeheader()
    for row in rows:
        writer.writerow({field: row.get(field, "") for field in CSV_FIELDS})
    _atomic_write_text(Path(path), output.getvalue())


def _read_json(path: Path) -> Any:
    with Path(path).open("r", encoding="utf-8") as handle:
        return json.load(handle)


def _read_queue(path: Path) -> list[dict[str, str]]:
    path = Path(path)
    if not path.exists():
        return []
    with path.open("r", encoding="utf-8", newline="") as handle:
        return [dict(row) for row in csv.DictReader(handle)]


def load_enquiries(path: Path) -> list[dict[str, Any]]:
    """Load a JSON array of enquiry objects."""

    payload = _read_json(Path(path))
    if not isinstance(payload, list):
        raise ValueError("Input must be a JSON array of enquiry objects")
    return [dict(item) if isinstance(item, Mapping) else item for item in payload]


def validate_enquiry(raw: Any, index: int) -> tuple[dict[str, Any] | None, dict[str, Any]]:
    """Validate and normalise one incoming enquiry."""

    if not isinstance(raw, Mapping):
        return None, {
            "index": index,
            "status": "invalid",
            "errors": ["record must be an object"],
        }

    errors: list[str] = []
    enquiry_id = _normalise_text(raw.get("enquiry_id", raw.get("id")))
    if not enquiry_id:
        errors.append("enquiry_id is required")

    received_at = _normalise_text(raw.get("received_at"))
    if not received_at:
        errors.append("received_at is required")
    else:
        try:
            datetime.fromisoformat(received_at.replace("Z", "+00:00"))
        except ValueError:
            errors.append("received_at must be ISO-8601")

    values = {field: _normalise_text(raw.get(field)) for field in REQUIRED_TEXT_FIELDS}
    for field, value in values.items():
        if not value:
            errors.append(f"{field} is required")
    if values["email"] and not EMAIL_RE.fullmatch(values["email"].lower()):
        errors.append("email must be a valid address")

    if errors:
        return None, {
            "index": index,
            "raw_id": enquiry_id or None,
            "status": "invalid",
            "errors": errors,
        }

    normalised = {
        "enquiry_id": enquiry_id,
        "received_at": received_at,
        "name": values["name"],
        "email": values["email"].lower(),
        "company": values["company"],
        "process": values["process"],
        "message": values["message"],
    }
    return normalised, {
        "index": index,
        "enquiry_id": enquiry_id,
        "status": "valid",
    }


def _append_to_queue(
    records: Iterable[Mapping[str, Any]],
    queue_path: Path,
    *,
    destination_available: bool,
) -> list[str]:
    """Append records once, atomically replacing the CSV queue."""

    queue_path = Path(queue_path)
    if not destination_available:
        raise DestinationUnavailable(str(queue_path))

    existing = _read_queue(queue_path)
    existing_ids = {row.get("enquiry_id", "") for row in existing}
    appended_ids: list[str] = []
    combined: list[Mapping[str, Any]] = list(existing)
    for record in records:
        enquiry_id = str(record["enquiry_id"])
        if enquiry_id in existing_ids:
            continue
        row = dict(record)
        row["status"] = "ready_for_follow_up"
        row["attempts"] = int(row.get("attempts", 1))
        combined.append(row)
        existing_ids.add(enquiry_id)
        appended_ids.append(enquiry_id)
    if appended_ids or not queue_path.exists():
        _atomic_write_csv(queue_path, combined)
    return appended_ids


def _write_recovery_file(
    retry_path: Path,
    records: Iterable[Mapping[str, Any]],
    *,
    reason: str,
    attempts: int = 1,
) -> None:
    _atomic_write_json(
        Path(retry_path),
        {
            "version": 1,
            "created_at_utc": utc_now(),
            "reason": reason,
            "attempts": attempts,
            "pending": [dict(record) for record in records],
            "recovered": [],
        },
    )


def process_enquiries(
    records: Iterable[Any],
    queue_path: Path,
    retry_path: Path,
    *,
    destination_available: bool = True,
    enabled: bool = True,
) -> dict[str, Any]:
    """Validate, deduplicate and queue enquiries, preserving failed work."""

    events: list[dict[str, Any]] = []
    unique: list[dict[str, Any]] = []
    seen_emails: dict[str, str] = {}
    invalid_count = 0
    duplicate_count = 0

    for index, raw in enumerate(records):
        normalised, validation_event = validate_enquiry(raw, index)
        if normalised is None:
            invalid_count += 1
            events.append(validation_event)
            continue

        email = normalised["email"]
        prior_id = seen_emails.get(email)
        if prior_id:
            duplicate_count += 1
            events.append(
                {
                    "index": index,
                    "enquiry_id": normalised["enquiry_id"],
                    "status": "duplicate",
                    "duplicate_of": prior_id,
                    "dedupe_key": email,
                }
            )
            continue
        seen_emails[email] = normalised["enquiry_id"]
        unique.append(normalised)
        events.append(validation_event)

    if not unique:
        return {
            "version": 1,
            "status": "complete",
            "accepted": 0,
            "invalid": invalid_count,
            "duplicates": duplicate_count,
            "pending_recovery": 0,
            "queued_ids": [],
            "events": events,
        }

    if not enabled:
        _write_recovery_file(retry_path, unique, reason="queue_disabled")
        events.append(
            {
                "status": "disabled",
                "pending_ids": [record["enquiry_id"] for record in unique],
            }
        )
        return {
            "version": 1,
            "status": "disabled",
            "accepted": len(unique),
            "invalid": invalid_count,
            "duplicates": duplicate_count,
            "pending_recovery": len(unique),
            "queued_ids": [],
            "events": events,
        }

    try:
        queued_ids = _append_to_queue(
            unique, queue_path, destination_available=destination_available
        )
    except DestinationUnavailable:
        _write_recovery_file(
            retry_path, unique, reason="destination_unavailable"
        )
        events.append(
            {
                "status": "destination_unavailable",
                "pending_ids": [record["enquiry_id"] for record in unique],
                "retry_path": str(retry_path),
            }
        )
        return {
            "version": 1,
            "status": "waiting_for_recovery",
            "accepted": len(unique),
            "invalid": invalid_count,
            "duplicates": duplicate_count,
            "pending_recovery": len(unique),
            "queued_ids": [],
            "events": events,
        }

    events.append({"status": "queued", "queued_ids": queued_ids})
    return {
        "version": 1,
        "status": "complete",
        "accepted": len(unique),
        "invalid": invalid_count,
        "duplicates": duplicate_count,
        "pending_recovery": 0,
        "queued_ids": queued_ids,
        "events": events,
    }


def recover_pending(
    retry_path: Path,
    queue_path: Path,
    *,
    destination_available: bool = True,
    enabled: bool = True,
) -> dict[str, Any]:
    """Recover pending work once, leaving a settled receipt in the retry file."""

    retry_path = Path(retry_path)
    if not retry_path.exists():
        return {"status": "no_pending", "recovered_ids": [], "pending": 0}

    payload = _read_json(retry_path)
    pending = list(payload.get("pending", []))
    if not pending:
        return {"status": "already_recovered", "recovered_ids": [], "pending": 0}
    if not enabled:
        return {
            "status": "disabled",
            "recovered_ids": [],
            "pending": len(pending),
        }
    if not destination_available:
        return {
            "status": "destination_unavailable",
            "recovered_ids": [],
            "pending": len(pending),
        }

    recovered_ids = _append_to_queue(
        pending, queue_path, destination_available=destination_available
    )
    payload["attempts"] = int(payload.get("attempts", 1)) + 1
    payload["recovered_at_utc"] = utc_now()
    payload["recovered"] = sorted(
        set(payload.get("recovered", [])) | set(recovered_ids)
    )
    payload["pending"] = []
    _atomic_write_json(retry_path, payload)
    return {
        "status": "recovered",
        "recovered_ids": recovered_ids,
        "pending": 0,
    }


def run_cli(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--input", required=True, type=Path)
    parser.add_argument("--queue", required=True, type=Path)
    parser.add_argument("--recovery", required=True, type=Path)
    parser.add_argument("--summary", required=True, type=Path)
    parser.add_argument(
        "--simulate-destination-outage",
        action="store_true",
        help="preserve valid records for a later recovery instead of writing the CSV",
    )
    parser.add_argument(
        "--recover",
        action="store_true",
        help="run deterministic recovery after the intake attempt",
    )
    parser.add_argument(
        "--disabled",
        action="store_true",
        help="disable the queue and preserve work in the recovery file",
    )
    args = parser.parse_args(argv)

    first = process_enquiries(
        load_enquiries(args.input),
        args.queue,
        args.recovery,
        destination_available=not args.simulate_destination_outage,
        enabled=not args.disabled,
    )
    result: dict[str, Any] = {"intake": first}
    if args.recover:
        result["recovery"] = recover_pending(
            args.recovery,
            args.queue,
            destination_available=True,
            enabled=not args.disabled,
        )
    _atomic_write_json(args.summary, result)
    print(json.dumps(result, indent=2, sort_keys=True))
    return 0


if __name__ == "__main__":
    sys.exit(run_cli())
