import json
import logging
import re
from datetime import date, datetime
from typing import Any, Dict, Optional
from urllib.parse import urlencode

import requests

logger = logging.getLogger(__name__)


API_PUSH_FIELDS = {
    "summary": "summary",
    "transcript": "transcript",
    "quality_score": "quality_score",
    "sentiment": "sentiment",
    "call_purpose": "call_purpose",
    "objection_type": "objection_type",
    "objections_concerns": "objections_concerns",
    "parameter_scores": "parameter_scores",
    "total_possible_score": "total_possible_score",
    "talk_listen_ratio": "talk_listen_ratio",
    "agent_name": "agent_name",
    "call_starttime": "call_starttime",
    "call_duration": "call_duration",
    "analysis_status": "analysis_status",
    "call_id": "call_id",
    "phone_number": "phone_number",
}

_TEMPLATE_PATTERN = re.compile(r"\{\{\s*([a-zA-Z0-9_]+)\s*\}\}")


def _json_safe(value: Any) -> Any:
    if isinstance(value, (datetime, date)):
        return value.isoformat()
    if isinstance(value, str):
        stripped = value.strip()
        if stripped and stripped[0] in "[{":
            try:
                return json.loads(stripped)
            except Exception:
                return value
    return value


def _append_query(url: str, key: str, value: str) -> str:
    separator = "&" if "?" in url else "?"
    return f"{url}{separator}{urlencode({key: value})}"


def _parse_effective_at(value) -> Optional[datetime]:
    if value is None:
        return None
    if isinstance(value, datetime):
        return value.replace(tzinfo=None) if value.tzinfo else value
    try:
        parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
        return parsed.replace(tzinfo=None) if parsed.tzinfo else parsed
    except Exception:
        return None


def _call_start_datetime(call_data: Dict[str, Any]) -> Optional[datetime]:
    raw = call_data.get("call_starttime") or call_data.get("call_start")
    return _parse_effective_at(raw)


class ApiPushService:
    def __init__(self, db_handler):
        self.db_handler = db_handler

    def push_call_update(
        self,
        bid: str,
        callid: str,
        analytics_data: Optional[Dict[str, Any]] = None,
        trigger_event: str = "analytics_saved",
        force: bool = False,
    ) -> Dict[str, Any]:
        config = self.db_handler.get_api_push_config(bid, include_secrets=True)
        if not config:
            return {"success": False, "skipped": True, "message": "API push is not configured"}
        if not force and not config.get("is_enabled"):
            return {"success": False, "skipped": True, "message": "API push is disabled"}
        if config.get("trigger_event") and config.get("trigger_event") != trigger_event and not force:
            return {"success": False, "skipped": True, "message": "Trigger does not match config"}
        if not force and self.db_handler.api_push_already_succeeded(bid, callid):
            return {"success": True, "skipped": True, "message": "Already pushed successfully"}

        endpoint_url = (config.get("endpoint_url") or "").strip()
        if not endpoint_url:
            return self._log_and_return(config, bid, callid, trigger_event, {}, "Endpoint URL is missing")

        call_data = self.db_handler.get_raw_call_details(bid, callid) or {}
        if not force:
            effective_at = _parse_effective_at(config.get("api_push_effective_at"))
            call_start = _call_start_datetime(call_data)
            if effective_at and call_start and call_start < effective_at:
                return {
                    "success": False,
                    "skipped": True,
                    "message": "Call predates API push effective date",
                }

        merged = self._merge_call_data(bid, callid, call_data, analytics_data or {})
        mapping_key = str(config.get("mapping_key") or "phone").lower()
        mapping_value = self._mapping_value(config, merged)
        if mapping_key not in ("none", "") and not mapping_value:
            return self._log_and_return(
                config,
                bid,
                callid,
                trigger_event,
                {},
                f"Mapping value missing for {mapping_key}",
            )

        payload = self._build_payload(config, merged)
        url, headers = self._build_request(config, endpoint_url, mapping_value)
        method = str(config.get("http_method") or "POST").upper()
        timeout = max(1, min(int(config.get("timeout_seconds") or 8), 30))
        content_type = str(config.get("content_type") or "application/json").strip() or "application/json"
        headers["Content-Type"] = content_type

        if mapping_key not in ("none", "") and config.get("mapping_location") == "body":
            payload[config.get("mapping_name") or config.get("mapping_key") or "mapping_key"] = mapping_value

        try:
            response = requests.request(method, url, json=payload, headers=headers, timeout=timeout)
            success = 200 <= response.status_code < 300
            self.db_handler.log_api_push_attempt(
                bid=bid,
                callid=callid,
                trigger_event=trigger_event,
                mapping_key=config.get("mapping_key"),
                mapping_value=mapping_value,
                endpoint_url=url,
                http_method=method,
                payload=payload,
                response_status=response.status_code,
                response_body=response.text[:5000],
                success=success,
                error_message=None if success else response.text[:1000],
            )
            return {
                "success": success,
                "status_code": response.status_code,
                "response_body": response.text[:1000],
                "payload": payload,
            }
        except Exception as exc:
            logger.warning("API push failed for bid=%s callid=%s: %s", bid, callid, exc)
            self.db_handler.log_api_push_attempt(
                bid=bid,
                callid=callid,
                trigger_event=trigger_event,
                mapping_key=config.get("mapping_key"),
                mapping_value=mapping_value,
                endpoint_url=url,
                http_method=method,
                payload=payload,
                success=False,
                error_message=str(exc),
            )
            return {"success": False, "error": str(exc), "payload": payload}

    def _log_and_return(self, config, bid, callid, trigger_event, payload, error):
        self.db_handler.log_api_push_attempt(
            bid=bid,
            callid=callid,
            trigger_event=trigger_event,
            mapping_key=config.get("mapping_key"),
            endpoint_url=config.get("endpoint_url"),
            http_method=config.get("http_method"),
            payload=payload,
            success=False,
            error_message=error,
        )
        return {"success": False, "error": error}

    def _merge_call_data(
        self,
        bid: str,
        callid: str,
        call_data: Dict[str, Any],
        analytics_data: Dict[str, Any],
    ) -> Dict[str, Any]:
        phone_number = (
            call_data.get("customer_callinfo")
            or call_data.get("customer_phone")
            or call_data.get("lead_phone")
            or ""
        )
        transcript = call_data.get("transcripts") or call_data.get("transcript") or ""
        quality_score = analytics_data.get("quality_score")
        summary = analytics_data.get("summary") or call_data.get("summary") or ""
        return {
            **call_data,
            **analytics_data,
            "bid": str(bid),
            "call_id": callid,
            "phone_number": phone_number,
            "transcript": transcript,
            "summary": summary,
            "quality_score": quality_score,
            "agent_name": call_data.get("agentname") or call_data.get("agent_name"),
            "call_duration": call_data.get("duration") or call_data.get("duration_seconds"),
            "analysis_status": "full_analysis" if quality_score is not None else "summary_only",
        }

    def _mapping_value(self, config: Dict[str, Any], merged: Dict[str, Any]) -> str:
        mapping_key = str(config.get("mapping_key") or "phone").lower()
        if mapping_key in ("none", ""):
            return ""
        if mapping_key == "call_id":
            return str(merged.get("call_id") or "")
        return str(merged.get("phone_number") or "")

    def _build_flat_payload(self, config: Dict[str, Any], merged: Dict[str, Any]) -> Dict[str, Any]:
        selected_fields = config.get("selected_fields") or ["summary"]
        field_mappings = config.get("field_mappings") or {}
        include_empty = bool(config.get("include_empty_fields"))
        payload = {}
        for field in selected_fields:
            if field not in API_PUSH_FIELDS:
                continue
            value = _json_safe(merged.get(API_PUSH_FIELDS[field]))
            if not include_empty and value in (None, "", [], {}):
                continue
            payload[field_mappings.get(field) or field] = value
        return payload

    def _render_template_value(self, field: str, merged: Dict[str, Any]) -> str:
        source_key = API_PUSH_FIELDS.get(field, field)
        value = _json_safe(merged.get(source_key))
        if value is None:
            return "null"
        if isinstance(value, (dict, list)):
            return json.dumps(value, ensure_ascii=False)
        if isinstance(value, bool):
            return "true" if value else "false"
        if isinstance(value, (int, float)):
            return str(value)
        return json.dumps(str(value), ensure_ascii=False)

    def _build_payload(self, config: Dict[str, Any], merged: Dict[str, Any]) -> Dict[str, Any]:
        payload_format = str(config.get("payload_format") or "flat_json").lower()
        flat_payload = self._build_flat_payload(config, merged)

        if payload_format == "wrapped_json":
            wrapper_key = str(config.get("payload_wrapper_key") or "data").strip() or "data"
            return {
                "bid": merged.get("bid"),
                "call_id": merged.get("call_id"),
                "phone_number": merged.get("phone_number"),
                wrapper_key: flat_payload,
            }

        if payload_format == "custom_template":
            template = str(config.get("payload_template") or "").strip()
            if not template:
                return flat_payload

            def _replace(match: re.Match) -> str:
                return self._render_template_value(match.group(1), merged)

            rendered = _TEMPLATE_PATTERN.sub(_replace, template)
            try:
                parsed = json.loads(rendered)
                if isinstance(parsed, dict):
                    return parsed
            except Exception as exc:
                logger.warning("Invalid custom API push template JSON: %s", exc)
            return flat_payload

        return flat_payload

    def _build_request(self, config: Dict[str, Any], endpoint_url: str, mapping_value: str):
        mapping_key = str(config.get("mapping_key") or "phone").lower()
        mapping_name = config.get("mapping_name") or config.get("mapping_key") or "mapping_key"
        url = endpoint_url
        if mapping_key not in ("none", "") and mapping_value:
            url = (
                endpoint_url
                .replace("{mapping_value}", mapping_value)
                .replace("{phone}", mapping_value)
                .replace("{call_id}", mapping_value)
            )
        headers = {"Content-Type": "application/json"}
        headers.update(config.get("custom_headers") or {})

        auth_type = config.get("auth_type")
        if auth_type == "bearer" and config.get("auth_token"):
            headers["Authorization"] = f"Bearer {config['auth_token']}"
        elif auth_type == "api_key" and config.get("api_key_name") and config.get("api_key_value"):
            headers[config["api_key_name"]] = config["api_key_value"]

        if mapping_key in ("none", "") or not mapping_value:
            return url, headers

        mapping_location = config.get("mapping_location") or "query"
        if mapping_location == "query":
            url = _append_query(url, mapping_name, mapping_value)
        elif mapping_location == "header":
            headers[mapping_name] = mapping_value

        return url, headers
