from __future__ import annotations

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

from autotouch_cli.exceptions import AutotouchCreditError, AutotouchInputError


@dataclass(frozen=True)
class ColumnCommandRuntime:
    resolve_token: Callable[[str | None, bool], str | None]
    request_api: Callable[..., Any]
    print_json: Callable[[Any, bool], None]
    load_json_input: Callable[..., Any]
    validate_column_contract_payload_or_exit: Callable[[Dict[str, Any]], None]
    validate_add_to_crm_create_payload: Callable[[Dict[str, Any]], None]
    build_projection_preflight_warnings: Callable[..., list[Dict[str, Any]]]
    print_projection_preflight_warnings: Callable[[list[Dict[str, Any]]], None]
    normalize_run_payload: Callable[[argparse.Namespace], Dict[str, Any]]
    run_precheck_for_add_to_crm: Callable[..., None]
    execute_run_flow: Callable[..., None]
    select_next_row_ids: Callable[..., Dict[str, Any]]


def _validate_position_flags(args: argparse.Namespace, *, allow_position: bool) -> None:
    if allow_position and getattr(args, "position", None) is not None:
        if getattr(args, "after_column_id", None) or getattr(args, "before_column_id", None):
            raise AutotouchInputError("--position cannot be combined with --after-column-id/--before-column-id")


def _apply_column_position_overrides(
    payload: Dict[str, Any],
    args: argparse.Namespace,
    *,
    allow_position: bool,
    allow_neighbors: bool,
) -> Dict[str, Any]:
    merged = dict(payload)
    _validate_position_flags(args, allow_position=allow_position)
    if allow_position and getattr(args, "position", None) is not None:
        merged["position"] = float(args.position)
        merged.pop("afterColumnId", None)
        merged.pop("after_column_id", None)
        merged.pop("beforeColumnId", None)
        merged.pop("before_column_id", None)
    if allow_neighbors:
        if getattr(args, "after_column_id", None):
            merged["afterColumnId"] = args.after_column_id
        if getattr(args, "before_column_id", None):
            merged["beforeColumnId"] = args.before_column_id
    return merged


def _normalize_http_request_test_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
    candidate = payload
    config = payload.get("config")
    if isinstance(config, dict):
        candidate = config
    if candidate.get("provider") and str(candidate.get("provider")) != "http_request":
        raise AutotouchInputError("columns test-http-request only accepts http_request payloads")

    url = candidate.get("url")
    if not isinstance(url, str) or not url.strip():
        raise AutotouchInputError("HTTP request preview requires url")

    headers = candidate.get("headers")
    if headers is None:
        headers = {}
    if not isinstance(headers, dict):
        raise AutotouchInputError("HTTP request preview requires headers to be a JSON object")

    timeout = candidate.get("timeout_seconds", candidate.get("timeoutSeconds", 30))
    normalized: Dict[str, Any] = {
        "method": str(candidate.get("method") or "GET").upper(),
        "url": url,
        "headers": headers,
        "body": candidate.get("body"),
        "timeoutSeconds": timeout,
    }
    return normalized


def cmd_columns_list(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "GET",
        f"/api/tables/{args.table_id}/columns",
        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_columns_create(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> 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("column create requires --data-json/--data-file with a JSON object")
    payload = _apply_column_position_overrides(
        payload,
        args,
        allow_position=True,
        allow_neighbors=True,
    )
    runtime.validate_column_contract_payload_or_exit(payload)
    runtime.validate_add_to_crm_create_payload(payload)
    data = runtime.request_api(
        "POST",
        f"/api/tables/{args.table_id}/columns",
        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_columns_stop(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "POST",
        f"/api/tables/{args.table_id}/columns/{args.column_id}/stop",
        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_columns_update(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> 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("column update requires --data-json/--data-file with a JSON object")
    payload = _apply_column_position_overrides(
        payload,
        args,
        allow_position=True,
        allow_neighbors=False,
    )
    runtime.validate_column_contract_payload_or_exit(payload)
    data = runtime.request_api(
        "PATCH",
        f"/api/tables/{args.table_id}/columns/{args.column_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_columns_delete(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> None:
    if not getattr(args, "yes", False):
        raise AutotouchInputError("column delete is destructive and also removes all cells for that column. Re-run with --yes to confirm.")

    token = runtime.resolve_token(args.token, required=True)
    data = runtime.request_api(
        "DELETE",
        f"/api/tables/{args.table_id}/columns/{args.column_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_columns_projections(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> 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("projection create requires --data-json/--data-file with a JSON object")
    payload = _apply_column_position_overrides(
        payload,
        args,
        allow_position=False,
        allow_neighbors=True,
    )

    preflight_warnings = runtime.build_projection_preflight_warnings(
        table_id=args.table_id,
        payload=payload,
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_projection_preflight_warnings(preflight_warnings)

    data = runtime.request_api(
        "POST",
        f"/api/tables/{args.table_id}/columns/projections",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    if preflight_warnings:
        runtime.print_json(
            {
                "event": "projections.created_with_warnings",
                "warnings": preflight_warnings,
                "result": data,
            },
            args.compact,
        )
        return
    runtime.print_json(data, args.compact)


def cmd_columns_move(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    if not args.after_column_id and not args.before_column_id:
        raise AutotouchInputError("columns move requires --after-column-id and/or --before-column-id")
    payload: Dict[str, Any] = {}
    if args.after_column_id:
        payload["after_column_id"] = args.after_column_id
    if args.before_column_id:
        payload["before_column_id"] = args.before_column_id
    data = runtime.request_api(
        "PATCH",
        f"/api/tables/{args.table_id}/columns/{args.column_id}/position",
        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_columns_test_http_request(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> 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("HTTP request preview requires --data-json/--data-file with a JSON object")
    data = runtime.request_api(
        "POST",
        f"/api/tables/{args.table_id}/test-http-request",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=_normalize_http_request_test_payload(payload),
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_columns_run(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = runtime.normalize_run_payload(args)
    runtime.run_precheck_for_add_to_crm(args=args, token=token, payload=payload)
    runtime.execute_run_flow(args=args, token=token, payload=payload, context=None)


def cmd_columns_run_next(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    requested_count = int(args.count or 0)
    if requested_count <= 0:
        raise AutotouchInputError("--count must be > 0")

    filters = runtime.load_json_input(
        inline_json=getattr(args, "filters_json", None),
        file_path=getattr(args, "filters_file", None),
        context="filters",
        default=None,
    )
    if filters is not None and not isinstance(filters, dict):
        raise AutotouchInputError("filters payload must be a JSON object")

    selection = runtime.select_next_row_ids(
        table_id=args.table_id,
        column_id=args.column_id,
        count=requested_count,
        filters=filters,
        unprocessed_only=bool(args.unprocessed_only),
        page_size=int(args.page_size or 200),
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    row_ids = selection.get("row_ids") or []
    if not row_ids:
        output = {
            "queued": False,
            "reason": "no eligible rows found for run-next selection",
            "selection": selection,
        }
        runtime.print_json(output, args.compact)
        if args.fail_if_empty:
            raise AutotouchCreditError("no eligible rows found for run-next selection")
        return

    payload: Dict[str, Any] = {
        "scope": "subset",
        "rowIds": row_ids,
        "unprocessedOnly": bool(args.unprocessed_only),
    }

    context = {
        "mode": "run-next",
        "selection": {k: v for k, v in selection.items() if k != "row_ids"},
        "row_ids": row_ids,
    }
    runtime.run_precheck_for_add_to_crm(args=args, token=token, payload=payload)
    runtime.execute_run_flow(args=args, token=token, payload=payload, context=context)


def cmd_columns_estimate(args: argparse.Namespace, *, runtime: ColumnCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    payload = runtime.normalize_run_payload(args)
    data = runtime.request_api(
        "POST",
        f"/api/tables/{args.table_id}/columns/{args.column_id}/estimate",
        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_columns_subcommands(
    subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
    *,
    add_api_common_arguments: Callable[[argparse.ArgumentParser], None],
    add_output_formatting_arguments: Callable[[argparse.ArgumentParser], None],
    add_run_scope_arguments: Callable[[argparse.ArgumentParser], None],
    add_run_execution_arguments: Callable[[argparse.ArgumentParser], None],
    handlers: Dict[str, Callable[[argparse.Namespace], None]],
    column_recipe_types: Sequence[str],
) -> None:
    pcol = subparsers.add_parser("columns", help="Column operations")
    col_sub = pcol.add_subparsers(dest="columns_cmd", required=True)

    pcl = col_sub.add_parser("list", help="List table columns")
    pcl.add_argument("--table-id", required=True)
    add_api_common_arguments(pcl)
    pcl.set_defaults(func=handlers["list"])

    pcc = col_sub.add_parser("create", help="Create a column")
    pcc.add_argument("--table-id", required=True)
    pcc.add_argument("--data-json", help="ColumnCreate payload JSON")
    pcc.add_argument("--data-file", help="ColumnCreate payload file path")
    pcc.add_argument("--position", type=float, help="Explicit numeric position override")
    pcc.add_argument("--after-column-id", help="Place the new column after this existing column id")
    pcc.add_argument("--before-column-id", help="Place the new column before this existing column id")
    add_api_common_arguments(pcc)
    pcc.set_defaults(func=handlers["create"])

    pcu = col_sub.add_parser(
        "update",
        help="Update a column",
        description=(
            "Update a column definition. Nested config keys merge into the existing config, "
            "so partial config patches are supported."
        ),
    )
    pcu.add_argument("--table-id", required=True)
    pcu.add_argument("--column-id", required=True)
    pcu.add_argument("--data-json", help="ColumnUpdate payload JSON")
    pcu.add_argument("--data-file", help="ColumnUpdate payload file path")
    pcu.add_argument("--position", type=float, help="Explicit numeric position override")
    add_api_common_arguments(pcu)
    pcu.set_defaults(func=handlers["update"])

    pcm = col_sub.add_parser(
        "move",
        help="Reorder an existing column relative to neighbors",
        description=(
            "Move an existing column by supplying neighbor column ids. "
            "Prefer sending both neighbors when possible for stable placement."
        ),
    )
    pcm.add_argument("--table-id", required=True)
    pcm.add_argument("--column-id", required=True)
    pcm.add_argument("--after-column-id", help="Place the column after this existing column id")
    pcm.add_argument("--before-column-id", help="Place the column before this existing column id")
    add_api_common_arguments(pcm)
    pcm.set_defaults(func=handlers["move"])

    pcd = col_sub.add_parser("delete", help="Delete a column and all of its cells")
    pcd.add_argument("--table-id", required=True)
    pcd.add_argument("--column-id", required=True)
    pcd.add_argument("--yes", action="store_true", help="Required confirmation flag for destructive delete")
    add_api_common_arguments(pcd)
    pcd.set_defaults(func=handlers["delete"])

    pcpj = col_sub.add_parser(
        "projections",
        help="Create JSON projection columns",
        description=(
            "Create JSON-split projection columns from a JSON source column.\n"
            "Use this command for flattening enrichment JSON output into readable columns.\n"
            "Do not use `autotouch columns create` with `kind: \"projection\"` and empty `config`."
        ),
        epilog=(
            "CreateProjectionsRequest payload example:\n\n"
            "{\n"
            "  \"items\": [\n"
            "    {\n"
            "      \"key\": \"company_name\",\n"
            "      \"label\": \"Company Name\",\n"
            "      \"sourceColumnId\": \"<JSON_COLUMN_ID>\",\n"
            "      \"path\": \"company_name\",\n"
            "      \"dataType\": \"text\"\n"
            "    }\n"
            "  ]\n"
            "}\n\n"
            "Each item must include `key`, `label`, `sourceColumnId`, and `path`."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    pcpj.add_argument("--table-id", required=True)
    pcpj.add_argument("--data-json", help="CreateProjectionsRequest payload JSON")
    pcpj.add_argument("--data-file", help="CreateProjectionsRequest payload file path")
    pcpj.add_argument("--after-column-id", help="Place the new projection group after this existing column id")
    pcpj.add_argument("--before-column-id", help="Place the new projection group before this existing column id")
    add_api_common_arguments(pcpj)
    pcpj.set_defaults(func=handlers["projections"])

    pcht = col_sub.add_parser(
        "test-http-request",
        help="Preview an HTTP Request column against the first table row",
        description=(
            "Resolve and execute one HTTP request using the first row in the table.\n"
            "This preview does not write cells.\n"
            "Pass either a full http_request column payload or a direct request config object."
        ),
        epilog=(
            "Recommended flow:\n\n"
            "autotouch columns recipe --type http_request --out-file column.json\n"
            "autotouch columns test-http-request --table-id <TABLE_ID> --data-file column.json\n"
            "autotouch columns create --table-id <TABLE_ID> --data-file column.json\n\n"
            "Template syntax:\n"
            "- {{column_key}} -> row value from the first table row\n"
            "- {{secrets.name}} -> workspace secret or active org integration credential by provider slug"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    pcht.add_argument("--table-id", required=True)
    pcht.add_argument("--data-json", help="HTTP request column payload JSON or direct preview payload JSON")
    pcht.add_argument("--data-file", help="HTTP request column payload file path or direct preview payload file path")
    add_api_common_arguments(pcht)
    pcht.set_defaults(func=handlers["test_http_request"])
    pcht._codex_examples = [
        "autotouch columns test-http-request --table-id <TABLE_ID> --data-file column.json",
    ]

    pcr = col_sub.add_parser("run", help="Run a column")
    pcr.add_argument("--table-id", required=True)
    pcr.add_argument("--column-id", required=True)
    add_run_scope_arguments(pcr)
    add_run_execution_arguments(pcr)
    add_api_common_arguments(pcr)
    pcr.set_defaults(func=handlers["run"])

    pcs = col_sub.add_parser("stop", help="Stop a running column (cancel active job dispatch)")
    pcs.add_argument("--table-id", required=True)
    pcs.add_argument("--column-id", required=True)
    add_api_common_arguments(pcs)
    pcs.set_defaults(func=handlers["stop"])

    pcrn = col_sub.add_parser("run-next", help="Run up to N selected rows using subset scope")
    pcrn.add_argument("--table-id", required=True)
    pcrn.add_argument("--column-id", required=True)
    pcrn.add_argument("--count", type=int, required=True, help="Maximum number of rows to queue")
    pcrn.add_argument("--filters-json", help="Optional JSON object to select from filtered rows")
    pcrn.add_argument("--filters-file", help="Optional JSON file to select from filtered rows")
    pcrn.add_argument("--page-size", type=int, default=200, help="Rows page size while selecting candidates (max 1000)")
    pcrn.add_argument(
        "--include-processed",
        dest="unprocessed_only",
        action="store_false",
        help="Include rows that already have output in candidate selection",
    )
    pcrn.add_argument("--fail-if-empty", action="store_true", help="Exit non-zero when no eligible rows are selected")
    pcrn.set_defaults(unprocessed_only=True)
    add_run_execution_arguments(pcrn)
    add_api_common_arguments(pcrn)
    pcrn.set_defaults(func=handlers["run_next"])

    pce = col_sub.add_parser("estimate", help="Estimate a column run")
    pce.add_argument("--table-id", required=True)
    pce.add_argument("--column-id", required=True)
    add_run_scope_arguments(pce)
    add_api_common_arguments(pce)
    pce.set_defaults(func=handlers["estimate"])

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