from __future__ import annotations

import argparse
from dataclasses import dataclass
from typing import Any, Callable, Dict, Optional, Sequence

from autotouch_cli.exceptions import (
    AutotouchAPIError,
    AutotouchInputError,
    AutotouchNotFoundError,
)
from autotouch_cli.sequence_support import (
    SequenceCommandInputError,
    SequenceRuntime,
    build_sequence_enroll_payload as build_sequence_enroll_payload_impl,
    sequence_mutation_params as sequence_mutation_params_impl,
)


@dataclass(frozen=True)
class SequenceCommandRuntime:
    resolve_token: Callable[[Optional[str], bool], Optional[str]]
    request_api: Callable[..., Any]
    print_json: Callable[[Any, bool], None]
    load_json_input: Callable[..., Any]
    api_url: Callable[[Optional[str]], str]
    emit_progress_event: Callable[[Any, bool], None]
    poll_job: Callable[..., Dict[str, Any]]
    default_timeout_seconds: int
    sequence_runtime_factory: Callable[[], SequenceRuntime]


def _sequence_mutation_params(args: argparse.Namespace) -> Optional[Dict[str, Any]]:
    return sequence_mutation_params_impl(args)


def _build_sequence_enroll_payload(
    args: argparse.Namespace,
    *,
    runtime: SequenceCommandRuntime,
) -> Dict[str, Any]:
    return build_sequence_enroll_payload_impl(args, runtime=runtime.sequence_runtime_factory())


def cmd_sequences_list(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    params: Dict[str, Any] = {
        "limit": max(1, int(getattr(args, "limit", 50) or 50)),
        "offset": max(0, int(getattr(args, "offset", 0) or 0)),
    }
    status = str(getattr(args, "status", "") or "").strip()
    source_table_id = str(getattr(args, "source_table_id", "") or "").strip()
    if status:
        params["status"] = status
    if source_table_id:
        params["sourceTableId"] = source_table_id
    if bool(getattr(args, "include_stats", False)):
        params["includeStats"] = True

    data = runtime.request_api(
        "GET",
        "/api/sequences",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        params=params,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_sequences_get(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "GET",
        f"/api/sequences/{args.sequence_id}",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_sequences_stats(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "GET",
        f"/api/sequences/{args.sequence_id}/stats",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_sequences_delivery_status(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "GET",
        "/api/sequences/delivery-status",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_sequences_create(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = runtime.load_json_input(
        inline_json=args.data_json,
        file_path=args.data_file,
        context="data",
        default=None,
    )
    if not isinstance(payload, dict):
        raise AutotouchInputError("sequence create requires --data-json/--data-file with a JSON object")

    data = runtime.request_api(
        "POST",
        "/api/sequences",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        params=_sequence_mutation_params(args),
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_sequences_update(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = runtime.load_json_input(
        inline_json=args.data_json,
        file_path=args.data_file,
        context="data",
        default=None,
    )
    if not isinstance(payload, dict):
        raise AutotouchInputError("sequence update requires --data-json/--data-file with a JSON object")

    data = runtime.request_api(
        "PUT",
        f"/api/sequences/{args.sequence_id}",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        params=_sequence_mutation_params(args),
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_sequences_delete(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    if not getattr(args, "yes", False):
        raise AutotouchInputError("sequence delete is destructive. Re-run with --yes to confirm. Only DRAFT or ARCHIVED sequences can be deleted.")

    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "DELETE",
        f"/api/sequences/{args.sequence_id}",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        params=_sequence_mutation_params(args),
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_sequences_clone(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "POST",
        f"/api/sequences/{args.sequence_id}/clone",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        params=_sequence_mutation_params(args),
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_sequences_status(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    target_status = str(getattr(args, "target_status", None) or getattr(args, "status", "") or "").strip().upper()
    if target_status not in {"ACTIVE", "PAUSED", "ARCHIVED"}:
        raise AutotouchInputError("--status must be one of ACTIVE, PAUSED, ARCHIVED")

    data = runtime.request_api(
        "PATCH",
        f"/api/sequences/{args.sequence_id}/status",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        params=_sequence_mutation_params(args),
        payload={"status": target_status},
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_sequences_enroll(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = runtime.load_json_input(
        inline_json=args.data_json,
        file_path=args.data_file,
        context="data",
        default=None,
    )
    if payload is None:
        try:
            payload = _build_sequence_enroll_payload(args, runtime=runtime)
        except SequenceCommandInputError as exc:
            raise AutotouchInputError(str(exc)) from exc

    if not isinstance(payload, dict):
        raise AutotouchInputError("sequence enroll payload must be a JSON object")

    data = runtime.request_api(
        "POST",
        f"/api/sequences/{args.sequence_id}/enroll",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        params=_sequence_mutation_params(args),
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )

    if not isinstance(data, dict):
        runtime.print_json(data, args.compact)
        return

    job_id = str(data.get("task_id") or data.get("job_id") or "").strip()
    status_url = f"{runtime.api_url(args.base_url)}/api/bulk-jobs/{job_id}" if job_id else None
    watch_command = f"autotouch jobs watch --job-id {job_id}" if job_id else None

    if not args.wait or not job_id:
        output = dict(data)
        if job_id:
            output.setdefault("job_id", job_id)
            output["job_status_url"] = status_url
            output["watch_command"] = watch_command
        runtime.print_json(output, args.compact)
        return

    if not args.quiet_wait:
        runtime.emit_progress_event(
            {
                "event": "sequence.enroll.wait_started",
                "sequence_id": args.sequence_id,
                "job_id": job_id,
                "task_id": job_id,
                "status": "polling_started",
                "status_url": status_url,
                "watch_command": watch_command,
                "hint": "polling /api/bulk-jobs/{job_id}",
            },
            args.compact,
        )

    poll_result = runtime.poll_job(
        job_id=job_id,
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        interval_seconds=int(args.poll_interval or 2),
        wait_timeout_seconds=int(args.wait_timeout or 0),
        request_timeout_seconds=int(args.timeout or runtime.default_timeout_seconds),
        verbose=args.verbose,
        compact=args.compact,
        once=False,
        print_updates=not args.quiet_wait,
    )
    final_job = poll_result.get("job") or {}
    timed_out = bool(poll_result.get("timed_out"))
    final_status = str((final_job or {}).get("status") or "").lower()
    output = {
        "event": "sequence.enroll.timed_out" if timed_out else "sequence.enroll.completed",
        "sequence_id": args.sequence_id,
        "enroll": data,
        "job": final_job,
        "final_job": final_job,
        "job_id": job_id,
        "task_id": job_id,
        "job_status_url": status_url,
        "watch_command": watch_command,
        "status": final_status,
        "timed_out": timed_out,
        "polls": int(poll_result.get("polls") or 0),
    }
    runtime.print_json(output, args.compact)

    if timed_out:
        raise AutotouchNotFoundError(f"enrollment job {job_id} timed out waiting for terminal status")
    if final_status in {"not_found", "unknown_not_found"}:
        raise AutotouchAPIError(f"enrollment job {job_id} disappeared before terminal status; verify sequence enrollments and rerun if needed")
    if args.fail_on_error and final_status in {"error", "cancelled"}:
        raise AutotouchAPIError(f"enrollment job {job_id} ended with status: {final_status}")
    if args.fail_on_partial and final_status == "partial":
        raise AutotouchAPIError(f"enrollment job {job_id} ended with partial status")


def cmd_sequences_pause_enrollment(args: argparse.Namespace, *, runtime: SequenceCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    lead_id = str(getattr(args, "lead_id", "") or "").strip()
    if not lead_id:
        raise AutotouchInputError("--lead-id is required")

    data = runtime.request_api(
        "POST",
        f"/api/sequences/{args.sequence_id}/enrollments/pause",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        params=_sequence_mutation_params(args),
        payload={"leadId": lead_id},
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def register_sequences_subcommands(
    subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
    *,
    add_api_common_arguments: Callable[[argparse.ArgumentParser], None],
    add_output_formatting_arguments: Callable[[argparse.ArgumentParser], None],
    add_actor_override_argument: Callable[[argparse.ArgumentParser], None],
    handlers: Dict[str, Callable[[argparse.Namespace], None]],
    sequence_recipe_types: Sequence[str],
) -> None:
    psq = subparsers.add_parser("sequences", help="Sequence management and enrollment")
    sequences_sub = psq.add_subparsers(dest="sequences_cmd", required=True)

    psql = sequences_sub.add_parser("list", help="List sequences")
    psql.add_argument("--status", help="Filter by status (DRAFT/ACTIVE/PAUSED/ARCHIVED)")
    psql.add_argument("--source-table-id", help="Filter by source table id")
    psql.add_argument("--include-stats", action="store_true", help="Include live enrollment stats per sequence")
    psql.add_argument("--limit", type=int, default=50, help="Max sequences to return (1-100)")
    psql.add_argument("--offset", type=int, default=0, help="Pagination offset")
    add_api_common_arguments(psql)
    psql.set_defaults(func=handlers["list"])

    psqg = sequences_sub.add_parser("get", help="Get one sequence")
    psqg.add_argument("--sequence-id", required=True)
    add_api_common_arguments(psqg)
    psqg.set_defaults(func=handlers["get"])

    psqs = sequences_sub.add_parser("stats", help="Get live stats for one sequence")
    psqs.add_argument("--sequence-id", required=True)
    add_api_common_arguments(psqs)
    psqs.set_defaults(func=handlers["stats"])

    psqd = sequences_sub.add_parser("delivery-status", help="Check sequence email delivery readiness")
    add_api_common_arguments(psqd)
    psqd.set_defaults(func=handlers["delivery_status"])

    psqr = sequences_sub.add_parser("recipe", help="Print copy-ready sequence payload templates")
    psqr.add_argument("--type", choices=["all", *sequence_recipe_types], default="all")
    psqr.add_argument("--out-file", help="Write template JSON to file (single type writes payload only)")
    add_output_formatting_arguments(psqr)
    psqr.set_defaults(func=handlers["recipe"])

    psqc = sequences_sub.add_parser("create", help="Create a sequence from a JSON payload")
    psqc.add_argument("--data-json", help="Sequence create payload JSON")
    psqc.add_argument("--data-file", help="Sequence create payload file path")
    add_actor_override_argument(psqc)
    add_api_common_arguments(psqc)
    psqc.set_defaults(func=handlers["create"])

    psqu = sequences_sub.add_parser("update", help="Update a sequence from a JSON payload")
    psqu.add_argument("--sequence-id", required=True)
    psqu.add_argument("--data-json", help="Sequence update payload JSON")
    psqu.add_argument("--data-file", help="Sequence update payload file path")
    add_actor_override_argument(psqu)
    add_api_common_arguments(psqu)
    psqu.set_defaults(func=handlers["update"])

    psqdel = sequences_sub.add_parser("delete", help="Delete a DRAFT or ARCHIVED sequence")
    psqdel.add_argument("--sequence-id", required=True)
    psqdel.add_argument("--yes", action="store_true", help="Required confirmation flag for destructive delete")
    add_actor_override_argument(psqdel)
    add_api_common_arguments(psqdel)
    psqdel.set_defaults(func=handlers["delete"])

    psqcl = sequences_sub.add_parser("clone", help="Clone a sequence into a new DRAFT")
    psqcl.add_argument("--sequence-id", required=True)
    add_actor_override_argument(psqcl)
    add_api_common_arguments(psqcl)
    psqcl.set_defaults(func=handlers["clone"])

    psqst = sequences_sub.add_parser("status", help="Set sequence status")
    psqst.add_argument("--sequence-id", required=True)
    psqst.add_argument("--status", choices=["ACTIVE", "PAUSED", "ARCHIVED"], required=True)
    add_actor_override_argument(psqst)
    add_api_common_arguments(psqst)
    psqst.set_defaults(func=handlers["status"])

    psqa = sequences_sub.add_parser("activate", help="Activate a sequence")
    psqa.add_argument("--sequence-id", required=True)
    add_actor_override_argument(psqa)
    add_api_common_arguments(psqa)
    psqa.set_defaults(func=handlers["status"], target_status="ACTIVE")

    psqp = sequences_sub.add_parser("pause", help="Pause a sequence")
    psqp.add_argument("--sequence-id", required=True)
    add_actor_override_argument(psqp)
    add_api_common_arguments(psqp)
    psqp.set_defaults(func=handlers["status"], target_status="PAUSED")

    psqar = sequences_sub.add_parser("archive", help="Archive a sequence")
    psqar.add_argument("--sequence-id", required=True)
    add_actor_override_argument(psqar)
    add_api_common_arguments(psqar)
    psqar.set_defaults(func=handlers["status"], target_status="ARCHIVED")

    psqe = sequences_sub.add_parser("enroll", help="Enroll leads into a sequence")
    psqe.add_argument("--sequence-id", required=True)
    psqe.add_argument("--data-json", help="Full enroll payload JSON")
    psqe.add_argument("--data-file", help="Full enroll payload JSON file")
    psqe.add_argument(
        "--lead-id",
        action="append",
        help="Lead id to enroll (repeatable; comma-separated also supported)",
    )
    psqe.add_argument("--lead-ids-json", help="JSON array or object with leadIds[]")
    psqe.add_argument("--lead-ids-file", help="Path to JSON/TXT/CSV file with lead ids")
    psqe.add_argument("--lead-ids-column", default="lead_id", help="CSV column for lead ids (default: lead_id)")
    psqe.add_argument("--start-date", help="Optional startDate ISO timestamp")
    psqe.add_argument("--research-context-json", help="researchContext JSON")
    psqe.add_argument("--research-context-file", help="researchContext JSON file")
    psqe.add_argument("--variable-map-json", help="variableMap JSON")
    psqe.add_argument("--variable-map-file", help="variableMap JSON file")
    psqe.add_argument("--variable-overrides-json", help="variableOverrides JSON")
    psqe.add_argument("--variable-overrides-file", help="variableOverrides JSON file")
    psqe.add_argument("--assigned-to-user-id", help="Optional explicit assignee user id")
    psqe.add_argument("--update-lead-owner", action="store_true", help="Also update Lead.user_id to the assignee")
    psqe.add_argument("--review-only", action="store_true", help="Create paused review-only enrollments")
    psqe.add_argument("--wait", action="store_true", help="Wait for enrollment bulk job terminal status")
    psqe.add_argument("--quiet-wait", action="store_true", help="Suppress per-poll output in --wait mode")
    psqe.add_argument("--poll-interval", type=int, default=2, help="Polling interval seconds for --wait")
    psqe.add_argument("--wait-timeout", type=int, default=0, help="Max seconds to wait (0 = no timeout)")
    psqe.add_argument("--fail-on-error", action="store_true", help="Exit non-zero when final status is error/cancelled")
    psqe.add_argument("--fail-on-partial", action="store_true", help="Exit non-zero when final status is partial")
    add_actor_override_argument(psqe)
    add_api_common_arguments(psqe)
    psqe.set_defaults(func=handlers["enroll"])

    psqep = sequences_sub.add_parser("pause-enrollment", help="Pause one lead's enrollment in a sequence")
    psqep.add_argument("--sequence-id", required=True)
    psqep.add_argument("--lead-id", required=True)
    add_actor_override_argument(psqep)
    add_api_common_arguments(psqep)
    psqep.set_defaults(func=handlers["pause_enrollment"])
