from __future__ import annotations

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

from autotouch_cli.exceptions import AutotouchInputError


@dataclass(frozen=True)
class LeadCommandRuntime:
    resolve_token: Callable[[Optional[str], bool], Optional[str]]
    request_api: Callable[..., Any]
    print_json: Callable[[Any, bool], None]
    load_required_object_payload: Callable[..., Dict[str, Any]]
    load_json_input: Callable[..., Any]
    split_cli_string_values: Callable[[Optional[List[str]]], List[str]]
    parse_string_list_payload: Callable[..., List[str]]
    dedupe_keep_order: Callable[[List[str]], List[str]]
    normalize_string_value: Callable[[Any], str]


def _normalize_filter_payload(filter_payload: Any) -> Any:
    if filter_payload is None:
        return None
    if isinstance(filter_payload, dict):
        return filter_payload
    raise AutotouchInputError("leads filter payload must be a JSON object")


def _normalize_sort_payload(sort_payload: Any) -> Any:
    if sort_payload is None:
        return None
    if isinstance(sort_payload, list):
        return sort_payload
    if isinstance(sort_payload, dict):
        return [sort_payload]
    raise AutotouchInputError("leads sort payload must be a JSON object or array")


def _load_string_list_input(
    *,
    runtime: LeadCommandRuntime,
    cli_values: Optional[List[str]],
    inline_json: Optional[str],
    file_path: Optional[str],
    context: str,
    key: Optional[str] = None,
) -> List[str]:
    values = runtime.split_cli_string_values(cli_values)
    payload = runtime.load_json_input(
        inline_json=inline_json,
        file_path=file_path,
        context=context,
        default=None,
    )
    if payload is not None:
        values.extend(runtime.parse_string_list_payload(payload, context=context, key=key))
    return runtime.dedupe_keep_order(values)


def _normalize_leads_query_payload(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> Dict[str, Any]:
    explicit_payload = runtime.load_json_input(
        inline_json=getattr(args, "data_json", None),
        file_path=getattr(args, "data_file", None),
        context="data",
        default=None,
    )
    if explicit_payload is not None:
        if not isinstance(explicit_payload, dict):
            raise AutotouchInputError("leads payload must be a JSON object")
        return explicit_payload

    payload: Dict[str, Any] = {}

    filter_payload = _normalize_filter_payload(
        runtime.load_json_input(
            inline_json=getattr(args, "filter_json", None),
            file_path=getattr(args, "filter_file", None),
            context="filter",
            default=None,
        )
    )
    if filter_payload is not None:
        payload["filter"] = filter_payload

    sort_payload = _normalize_sort_payload(
        runtime.load_json_input(
            inline_json=getattr(args, "sort_json", None),
            file_path=getattr(args, "sort_file", None),
            context="sort",
            default=None,
        )
    )
    if sort_payload is not None:
        payload["sort"] = sort_payload

    search = str(getattr(args, "search", "") or "").strip()
    if search:
        payload["search"] = search

    pagination: Dict[str, Any] = {}
    limit = getattr(args, "limit", None)
    if limit is not None:
        pagination["limit"] = int(limit)
    cursor = str(getattr(args, "cursor", "") or "").strip()
    if cursor:
        pagination["cursor"] = cursor
    if pagination:
        payload["pagination"] = pagination

    scope = str(getattr(args, "scope", "") or "").strip()
    if scope:
        payload["scope"] = scope

    source_table_id = str(getattr(args, "source_table_id", "") or "").strip()
    if source_table_id:
        payload["source_table_id"] = source_table_id

    source_table_scope = str(getattr(args, "source_table_scope", "") or "").strip()
    if source_table_scope:
        payload["source_table_scope"] = source_table_scope

    related_tables_mode = str(getattr(args, "related_tables_mode", "") or "").strip()
    if related_tables_mode:
        payload["related_tables_mode"] = related_tables_mode

    if bool(getattr(args, "include_research_data", False)):
        payload["include_research_data"] = True

    if bool(getattr(args, "include_placeholder_leads", False)):
        payload["include_placeholder_leads"] = True

    return payload


def _normalize_leads_research_payload(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> Dict[str, Any]:
    explicit_payload = runtime.load_json_input(
        inline_json=getattr(args, "data_json", None),
        file_path=getattr(args, "data_file", None),
        context="data",
        default=None,
    )
    if explicit_payload is not None:
        if not isinstance(explicit_payload, dict):
            raise AutotouchInputError("leads research payload must be a JSON object")
        return explicit_payload

    lead_ids = _load_string_list_input(
        runtime=runtime,
        cli_values=getattr(args, "lead_id", None),
        inline_json=getattr(args, "lead_ids_json", None),
        file_path=getattr(args, "lead_ids_file", None),
        context="lead_ids",
        key="lead_ids",
    )
    if not lead_ids:
        raise AutotouchInputError("provide at least one lead id")

    source_table_id = str(getattr(args, "source_table_id", "") or "").strip()
    if not source_table_id:
        raise AutotouchInputError("--source-table-id is required")

    payload: Dict[str, Any] = {
        "lead_ids": lead_ids,
        "source_table_id": source_table_id,
    }

    field_ids = _load_string_list_input(
        runtime=runtime,
        cli_values=getattr(args, "field_id", None),
        inline_json=getattr(args, "field_ids_json", None),
        file_path=getattr(args, "field_ids_file", None),
        context="field_ids",
        key="field_ids",
    )
    if field_ids:
        payload["field_ids"] = field_ids

    return payload


def _normalize_leads_bulk_target_payload(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> Dict[str, Any]:
    lead_ids = _load_string_list_input(
        runtime=runtime,
        cli_values=getattr(args, "lead_id", None),
        inline_json=getattr(args, "lead_ids_json", None),
        file_path=getattr(args, "lead_ids_file", None),
        context="lead_ids",
        key="lead_ids",
    )
    if lead_ids:
        return {"lead_ids": lead_ids}

    filter_payload = _normalize_filter_payload(
        runtime.load_json_input(
            inline_json=getattr(args, "filter_json", None),
            file_path=getattr(args, "filter_file", None),
            context="filter",
            default=None,
        )
    )
    sort_payload = _normalize_sort_payload(
        runtime.load_json_input(
            inline_json=getattr(args, "sort_json", None),
            file_path=getattr(args, "sort_file", None),
            context="sort",
            default=None,
        )
    )

    search = str(getattr(args, "search", "") or "").strip()
    scope = str(getattr(args, "scope", "") or "").strip()
    source_table_id = str(getattr(args, "source_table_id", "") or "").strip()
    source_table_scope = str(getattr(args, "source_table_scope", "") or "").strip()
    limit = getattr(args, "limit", None)

    if (
        filter_payload is None
        and sort_payload is None
        and not search
        and not scope
        and not source_table_id
        and not source_table_scope
        and limit is None
    ):
        raise AutotouchInputError("provide lead ids or descriptor selection input via --filter-json/--filter-file/--search")

    selection: Dict[str, Any] = {
        "mode": "descriptor",
        "descriptor": filter_payload or {"root": {"kind": "group", "logic": "and", "children": []}},
    }
    if sort_payload is not None:
        selection["sort"] = sort_payload
    if search:
        selection["search"] = search
    if scope:
        selection["scope"] = scope
    if source_table_id:
        selection["source_table_id"] = source_table_id
    if source_table_scope:
        selection["source_table_scope"] = source_table_scope
    if limit is not None:
        selection["limit"] = max(1, int(limit))

    return {"selection": selection}


def _load_leads_bulk_action_payload(
    args: argparse.Namespace,
    *,
    required_field: str,
    cli_value: Optional[str],
    runtime: LeadCommandRuntime,
) -> Dict[str, Any]:
    explicit_payload = runtime.load_json_input(
        inline_json=getattr(args, "data_json", None),
        file_path=getattr(args, "data_file", None),
        context="data",
        default=None,
    )
    if explicit_payload is not None:
        if not isinstance(explicit_payload, dict):
            raise AutotouchInputError("bulk lead action payload must be a JSON object")
        payload = dict(explicit_payload)
    else:
        payload = _normalize_leads_bulk_target_payload(args, runtime=runtime)

    value = runtime.normalize_string_value(cli_value if cli_value is not None else payload.get(required_field))
    if not value:
        raise AutotouchInputError(f"provide --{required_field.replace('_', '-')} or include '{required_field}' in --data-json/--data-file")
    payload[required_field] = value
    return payload


def cmd_leads_query(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = _normalize_leads_query_payload(args, runtime=runtime)
    data = runtime.request_api(
        "POST",
        "/api/leads/query",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_leads_get(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "GET",
        f"/api/leads/{args.lead_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_leads_create(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = runtime.load_required_object_payload(args, noun="lead create")
    data = runtime.request_api(
        "POST",
        "/api/leads",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_leads_update(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = runtime.load_required_object_payload(args, noun="lead update")
    data = runtime.request_api(
        "PUT",
        f"/api/leads/{args.lead_id}",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_leads_upsert_contact_channels(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = runtime.load_required_object_payload(args, noun="lead contact upsert")
    data = runtime.request_api(
        "POST",
        f"/api/leads/{args.lead_id}/contact-channels",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_leads_count(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = _normalize_leads_query_payload(args, runtime=runtime)
    data = runtime.request_api(
        "POST",
        "/api/leads/query/count",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_leads_stats(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = _normalize_leads_query_payload(args, runtime=runtime)
    data = runtime.request_api(
        "POST",
        "/api/leads/query/stats",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_leads_research(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = _normalize_leads_research_payload(args, runtime=runtime)
    data = runtime.request_api(
        "POST",
        "/api/leads/query/research",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_leads_filters_metadata(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "GET",
        "/api/leads/filters/metadata",
        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_leads_research_tables(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "GET",
        "/api/leads/research-tables/available",
        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_leads_update_status(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = _load_leads_bulk_action_payload(
        args,
        required_field="status",
        cli_value=getattr(args, "status", None),
        runtime=runtime,
    )
    data = runtime.request_api(
        "POST",
        "/api/leads/bulk_update_status",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_leads_reassign_owner(args: argparse.Namespace, *, runtime: LeadCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = _load_leads_bulk_action_payload(
        args,
        required_field="user_id",
        cli_value=getattr(args, "user_id", None),
        runtime=runtime,
    )
    data = runtime.request_api(
        "POST",
        "/api/leads/bulk_reassign_owner",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def register_leads_subcommands(
    subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
    *,
    add_api_common_arguments: Callable[[argparse.ArgumentParser], None],
    add_leads_selection_arguments: Callable[[argparse.ArgumentParser], None],
    add_leads_query_arguments: Callable[[argparse.ArgumentParser], None],
    add_leads_research_arguments: Callable[[argparse.ArgumentParser], None],
    handlers: Dict[str, Callable[[argparse.Namespace], None]],
    lead_recipe_types: Sequence[str],
) -> None:
    parser = subparsers.add_parser("leads", help="Lead query + mutation operations")
    leads_sub = parser.add_subparsers(dest="leads_cmd", required=True)

    recipe_parser = leads_sub.add_parser(
        "recipe",
        help="Print lead payload recipes for create/update/query/research/bulk selection",
    )
    recipe_parser.add_argument("--type", choices=["all", *lead_recipe_types], default="all")
    recipe_parser.add_argument("--out-file", help="Optional path to save the selected payload JSON")
    add_api_common_arguments(recipe_parser)
    recipe_parser.set_defaults(func=handlers["recipe"])

    get_parser = leads_sub.add_parser("get", help="Get one lead by id")
    get_parser.add_argument("--lead-id", required=True)
    add_api_common_arguments(get_parser)
    get_parser.set_defaults(func=handlers["get"])

    create_parser = leads_sub.add_parser("create", help="Create a lead from a JSON payload")
    create_parser.add_argument("--data-json", help="Lead create payload JSON")
    create_parser.add_argument("--data-file", help="Path to lead create payload JSON file")
    add_api_common_arguments(create_parser)
    create_parser.set_defaults(func=handlers["create"])

    update_parser = leads_sub.add_parser("update", help="Update one lead with a partial JSON payload")
    update_parser.add_argument("--lead-id", required=True)
    update_parser.add_argument("--data-json", help="Lead update payload JSON")
    update_parser.add_argument("--data-file", help="Path to lead update payload JSON file")
    add_api_common_arguments(update_parser)
    update_parser.set_defaults(func=handlers["update"])

    channels_parser = leads_sub.add_parser(
        "upsert-contact-channels",
        aliases=["upsert-contacts"],
        help="Merge emails/phones into a lead without replacing the full arrays",
    )
    channels_parser.add_argument("--lead-id", required=True)
    channels_parser.add_argument("--data-json", help="Contact upsert payload JSON")
    channels_parser.add_argument("--data-file", help="Path to contact upsert payload JSON file")
    add_api_common_arguments(channels_parser)
    channels_parser.set_defaults(func=handlers["upsert_contact_channels"])

    update_status_parser = leads_sub.add_parser(
        "update-status",
        help="Bulk update lead status by ids or descriptor selection",
    )
    add_leads_selection_arguments(update_status_parser)
    update_status_parser.add_argument("--status", help="Lead status to apply")
    add_api_common_arguments(update_status_parser)
    update_status_parser.set_defaults(func=handlers["update_status"])

    reassign_parser = leads_sub.add_parser(
        "reassign-owner",
        help="Bulk reassign lead owner by ids or descriptor selection",
    )
    add_leads_selection_arguments(reassign_parser)
    reassign_parser.add_argument("--user-id", help="Target owner user id")
    add_api_common_arguments(reassign_parser)
    reassign_parser.set_defaults(func=handlers["reassign_owner"])

    query_parser = leads_sub.add_parser("query", help="Query/filter leads with descriptor payloads")
    add_leads_query_arguments(query_parser)
    add_api_common_arguments(query_parser)
    query_parser.set_defaults(func=handlers["query"])

    count_parser = leads_sub.add_parser("count", help="Count leads matching a query payload")
    add_leads_query_arguments(count_parser)
    add_api_common_arguments(count_parser)
    count_parser.set_defaults(func=handlers["count"])

    stats_parser = leads_sub.add_parser("stats", help="Aggregate stats for leads matching a query payload")
    add_leads_query_arguments(stats_parser)
    add_api_common_arguments(stats_parser)
    stats_parser.set_defaults(func=handlers["stats"])

    research_parser = leads_sub.add_parser("research", help="Fetch research row data for specific lead ids")
    add_leads_research_arguments(research_parser)
    add_api_common_arguments(research_parser)
    research_parser.set_defaults(func=handlers["research"])

    filters_parser = leads_sub.add_parser("filters-metadata", help="List filterable lead fields and research columns")
    add_api_common_arguments(filters_parser)
    filters_parser.set_defaults(func=handlers["filters_metadata"])

    research_tables_parser = leads_sub.add_parser(
        "research-tables",
        help="List research tables available for lead lookups",
    )
    add_api_common_arguments(research_tables_parser)
    research_tables_parser.set_defaults(func=handlers["research_tables"])
