"""Batch helpers for Bitrix24 webhook requests."""

import copy
import time
from typing import Any
from urllib.parse import quote_plus, urlsplit

from .config import get_config
from .constants import (
    DEFAULT_BATCH_SIZE,
    DEFAULT_PAGE_SIZE,
    ERR_BATCH_ITEMS_NOT_FOUND,
    ERR_BATCH_STEP,
    ERR_EMPTY_COMMANDS,
    ERR_EMPTY_PARAMS_LIST,
    ERR_INVALID_PARAMS_ITEM,
    ERR_TOO_MANY_COMMANDS,
    ERROR_CODE_BATCH_STEP,
    ERROR_CODE_EMPTY_COMMANDS,
    ERROR_CODE_EMPTY_PARAMS_LIST,
    ERROR_CODE_INVALID_PARAMS_ITEM,
    ERROR_CODE_ITEMS_NOT_FOUND,
    ERROR_CODE_TOO_MANY_COMMANDS,
    ERROR_TYPE_BATCH,
    ERROR_TYPE_BATCH_PARSE,
    ERROR_TYPE_CONFIGURATION,
    LOG_BATCH_BY_PARAMS_STEP_ERRORS,
    LOG_BATCH_LIST_ITEMS_NOT_FOUND,
    LOG_BATCH_LIST_STEP_ERRORS,
    LOG_BATCH_REQUEST_STARTED,
    )
from .logging_utils import get_logger
from .rest import request24

logger = get_logger()


def _sanitize_portal(webhook_url: str) -> str:
    url = (webhook_url or "").strip()
    if not url:
        return ""
    parts = urlsplit(url)
    if parts.netloc:
        return parts.netloc
    if "/rest/" in url:
        return url.split("/rest/")[0].rstrip("/").split("//")[-1]
    return url.rstrip("/").split("//")[-1]


def _error_dict(
        *,
        error_type: str,
        error_code: str,
        error_message: str,
        method: str,
        webhook_url: str,
        details: Any | None = None,
        ) -> dict[str, Any]:
    result: dict[str, Any] = {
        "error_result": True,
        "error_type": error_type,
        "error_code": error_code,
        "error_message": error_message,
        "method": method,
        "portal": _sanitize_portal(webhook_url),
        }
    if details is not None:
        result["details"] = details
    return result


def _iter_query_items(value: Any, prefix: str) -> list[tuple[str, str]]:
    items: list[tuple[str, str]] = []

    if isinstance(value, dict):
        for key, item in value.items():
            next_prefix = f"{prefix}[{key}]" if prefix else str(key)
            items.extend(_iter_query_items(item, next_prefix))
        return items

    if isinstance(value, list):
        for index, item in enumerate(value):
            next_prefix = f"{prefix}[{index}]"
            items.extend(_iter_query_items(item, next_prefix))
        return items

    if value is None:
        return [(prefix, "")]

    return [(prefix, str(value))]


def _dict_to_query(parameters: dict[str, Any] | None) -> str:
    if not parameters:
        return ""

    query_items: list[str] = []
    for key, value in parameters.items():
        for item_key, item_value in _iter_query_items(value, str(key)):
            query_items.append(f"{quote_plus(item_key)}={quote_plus(item_value)}")
    return "&".join(query_items)


def _build_batch_command(method: str, parameters: dict[str, Any] | None = None) -> str:
    query_string = _dict_to_query(parameters)
    if not query_string:
        return method
    return f"{method}?{query_string}"


def _extract_step_items(step_payload: Any, items_key: str | None = None) -> tuple[list[Any] | None, str | None]:
    if isinstance(step_payload, list):
        return step_payload, None

    if isinstance(step_payload, dict):
        resolved_key = items_key
        if not resolved_key:
            for key, value in step_payload.items():
                if isinstance(value, list):
                    resolved_key = key
                    break
        if resolved_key and isinstance(step_payload.get(resolved_key), list):
            return step_payload.get(resolved_key), resolved_key

    return None, items_key


def _extract_batch_result(payload: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
    result_root = payload.get("result")
    if not isinstance(result_root, dict):
        return {}, {}, {}

    result_map = result_root.get("result")
    if not isinstance(result_map, dict):
        result_map = {}

    result_error = result_root.get("result_error")
    if not isinstance(result_error, dict):
        result_error = {}

    return result_root, result_map, result_error


def _batch_request(
        commands: dict[str, str],
        webhook_url: str | None = None,
        *,
        timeout: int | None = None,
        halt: int = 1,
        ) -> dict[str, Any]:
    """Run raw Bitrix24 batch request."""

    if not isinstance(commands, dict) or not commands:
        return _error_dict(
            error_type=ERROR_TYPE_CONFIGURATION,
            error_code=ERROR_CODE_EMPTY_COMMANDS,
            error_message=ERR_EMPTY_COMMANDS,
            method="batch",
            webhook_url=webhook_url or "",
            )

    if len(commands) > DEFAULT_BATCH_SIZE:
        return _error_dict(
            error_type=ERROR_TYPE_CONFIGURATION,
            error_code=ERROR_CODE_TOO_MANY_COMMANDS,
            error_message=ERR_TOO_MANY_COMMANDS,
            method="batch",
            webhook_url=webhook_url or "",
            details={"commands_count": len(commands)},
            )

    logger.debug(
        LOG_BATCH_REQUEST_STARTED,
        _sanitize_portal(webhook_url or ""),
        list(commands.keys()),
        )
    return request24(
        "batch",
        {"halt": halt, "cmd": commands},
        webhook_url,
        timeout=timeout,
        )


def batch_by_params(
        method: str,
        params_list: list[dict[str, Any]],
        webhook_url: str | None = None,
        *,
        timeout: int | None = None,
        halt: int = 1,
        ) -> dict[str, Any]:
    """Run the same Bitrix24 method in batch mode for a list of parameter dicts."""
    config = get_config()

    if not isinstance(params_list, list) or not params_list:
        return _error_dict(
            error_type=ERROR_TYPE_CONFIGURATION,
            error_code=ERROR_CODE_EMPTY_PARAMS_LIST,
            error_message=ERR_EMPTY_PARAMS_LIST,
            method=method,
            webhook_url=webhook_url or "",
            )

    for index, item in enumerate(params_list):
        if not isinstance(item, dict):
            return _error_dict(
                error_type=ERROR_TYPE_CONFIGURATION,
                error_code=ERROR_CODE_INVALID_PARAMS_ITEM,
                error_message=ERR_INVALID_PARAMS_ITEM,
                method=method,
                webhook_url=webhook_url or "",
                details={"index": index, "value_type": type(item).__name__},
                )

    aggregated: list[Any] = []

    for chunk_start in range(0, len(params_list), DEFAULT_BATCH_SIZE):
        chunk = params_list[chunk_start:chunk_start + DEFAULT_BATCH_SIZE]
        commands: dict[str, str] = {}
        for index, item_params in enumerate(chunk):
            commands[f"step_{index}"] = _build_batch_command(method, item_params)

        response = _batch_request(commands, webhook_url, timeout=timeout, halt=halt)
        if "error_result" in response:
            return response

        _, result_map, result_error = _extract_batch_result(response)
        if result_error:
            logger.warning(
                LOG_BATCH_BY_PARAMS_STEP_ERRORS,
                method,
                _sanitize_portal(webhook_url or ""),
                result_error,
                )
            return _error_dict(
                error_type=ERROR_TYPE_BATCH,
                error_code=ERROR_CODE_BATCH_STEP,
                error_message=ERR_BATCH_STEP,
                method=method,
                webhook_url=webhook_url or "",
                details=result_error,
                )

        for step_name in commands.keys():
            if step_name in result_map:
                aggregated.append(result_map[step_name])

        has_more_chunks = chunk_start + DEFAULT_BATCH_SIZE < len(params_list)
        if has_more_chunks and config.request_delay > 0:
            time.sleep(config.request_delay)

    return {
        "result": aggregated,
        "total": len(aggregated),
        }


def batch_list(
        method: str,
        parameters: dict[str, Any] | None = None,
        webhook_url: str | None = None,
        *,
        timeout: int | None = None,
        items_key: str | None = None,
        halt: int = 1,
        max_batches: int | None = None,
        ) -> dict[str, Any]:
    """Read list methods in batch mode with offset pagination."""

    config = get_config()
    params = copy.deepcopy(parameters) if parameters else {}
    base_start = int(params.get("start", 0) or 0)
    aggregated: list[Any] = []
    resolved_items_key = items_key
    template_payload: Any = None
    batches_ran = 0

    while True:
        commands: dict[str, str] = {}
        for batch_index in range(DEFAULT_BATCH_SIZE):
            command_params = copy.deepcopy(params)
            command_params["start"] = base_start + (batch_index * DEFAULT_PAGE_SIZE)
            commands[f"step_{batch_index}"] = _build_batch_command(method, command_params)

        response = _batch_request(commands, webhook_url, timeout=timeout, halt=halt)
        if "error_result" in response:
            return response

        _, result_map, result_error = _extract_batch_result(response)
        if result_error:
            logger.warning(
                LOG_BATCH_LIST_STEP_ERRORS,
                method,
                _sanitize_portal(webhook_url or ""),
                result_error,
                )
            return _error_dict(
                error_type=ERROR_TYPE_BATCH,
                error_code=ERROR_CODE_BATCH_STEP,
                error_message=ERR_BATCH_STEP,
                method=method,
                webhook_url=webhook_url or "",
                details=result_error,
                )

        should_stop = False
        for step_name in commands.keys():
            if step_name not in result_map:
                should_stop = True
                break

            step_payload = result_map[step_name]
            if template_payload is None:
                template_payload = copy.deepcopy(step_payload)

            items, resolved_items_key = _extract_step_items(step_payload, resolved_items_key)
            if items is None:
                logger.warning(
                    LOG_BATCH_LIST_ITEMS_NOT_FOUND,
                    method,
                    _sanitize_portal(webhook_url or ""),
                    step_name,
                    )
                return _error_dict(
                    error_type=ERROR_TYPE_BATCH_PARSE,
                    error_code=ERROR_CODE_ITEMS_NOT_FOUND,
                    error_message=ERR_BATCH_ITEMS_NOT_FOUND,
                    method=method,
                    webhook_url=webhook_url or "",
                    details={"step": step_name},
                    )

            aggregated.extend(items)

            if len(items) < DEFAULT_PAGE_SIZE:
                should_stop = True
                break

        batches_ran += 1
        if should_stop:
            break

        if max_batches is not None and batches_ran >= max_batches:
            break

        base_start += DEFAULT_BATCH_SIZE * DEFAULT_PAGE_SIZE
        if config.request_delay > 0:
            time.sleep(config.request_delay)

    if isinstance(template_payload, dict) and resolved_items_key:
        final_result = copy.deepcopy(template_payload)
        final_result[resolved_items_key] = aggregated
        final_result.pop("next", None)
        final_result.pop("next_id", None)
        final_result.pop("nextId", None)
        return {
            "result": final_result,
            "total": len(aggregated),
            }

    return {
        "result": aggregated,
        "total": len(aggregated),
        }
