from __future__ import annotations

import asyncio
import logging
from datetime import timedelta
from typing import Annotated, Any

import httpx
from dateutil import parser as date_parser
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from sqlalchemy import text
from sqlalchemy.orm import Session

from app.config import build_mcube_callback_url, build_mcube_refurl_for_call, get_settings
from app.database import get_db_cluster, get_db_default, session_scope_cluster
from app.dependencies import AuthUser, get_current_user
from app.repositories.schema_helper import has_column, has_table
from app.schemas.campaign import CampaignCreateBody
from app.services import campaign_core
from app.services.audit_log import audit_trail
from app.services.campaign_core import (
    check_business_time_window,
    check_remaining_minutes,
    compute_waiting_time_for_new_campaign,
    fetch_executive_number,
)
from app.utils.cluster_table_names import campaign_contacts_table, campaigns_table
from app.utils.csv_parse import parse_csv_content
from app.utils.excel_parse import workbook_bytes_to_contact_rows
from app.utils.queue_dispatch import dispatch_campaign_to_queue
from app.utils.timezone_util import format_dt, now_app

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/campaigns", tags=["campaigns"])


def _php_filter_bool(v: Any) -> bool:
    if isinstance(v, bool):
        return v
    s = str(v).strip().lower()
    return s in ("1", "true", "on", "yes")


async def _auto_call_contacts_after_upload(
    *,
    business_id: int,
    campaign_id: int,
    contact_ids: list[int],
    agent_name: str,
    callback_url: str | None,
    refurl: str | None,
    delay_seconds: float,
) -> None:
    """
    Fire outbound calls sequentially for the uploaded contacts.

    - Reads `number` from `{business_id}_campaign_contacts`
    - Calls MCube outbound URL (`mcube_local_outbound_url`, default `https://app3.syntheon.in/api/mcube/outbound-call`)
    - Sleeps `delay_seconds` between each trigger
    - Stores returned `mcube_call_sid` into `call_id` when available
    """
    settings = get_settings()
    contacts_table = campaign_contacts_table(int(business_id))

    # Clamp delay to a safe range so accidental inputs don't DOS the process.
    delay_s = float(delay_seconds)
    if delay_s < 0.0:
        delay_s = 0.0
    if delay_s > 60.0:
        delay_s = 60.0

    async with httpx.AsyncClient(timeout=httpx.Timeout(20.0)) as client:
        for i, contact_id in enumerate(contact_ids):
            try:
                with session_scope_cluster() as db:
                    row = db.execute(
                        text(
                            f"SELECT id, number, name, status FROM `{contacts_table}` "
                            "WHERE id = :id AND campaign_id = :cid AND business_id = :bid LIMIT 1"
                        ),
                        {"id": int(contact_id), "cid": int(campaign_id), "bid": int(business_id)},
                    ).mappings().first()

                    if not row:
                        continue

                    number = str(row.get("number") or "").strip()
                    if not number:
                        db.execute(
                            text(
                                f"UPDATE `{contacts_table}` SET status = 'failed', updated_at = :u "
                                "WHERE id = :id AND campaign_id = :cid AND business_id = :bid"
                            ),
                            {"u": now_app(), "id": int(contact_id), "cid": int(campaign_id), "bid": int(business_id)},
                        )
                        continue

                    # Store a temporary correlation id so the UI can show "calling" immediately.
                    refid = f"cmp_{campaign_id}_contact_{contact_id}"
                    db.execute(
                        text(
                            f"UPDATE `{contacts_table}` SET status = 'calling', call_id = :call_id, updated_at = :u "
                            "WHERE id = :id AND campaign_id = :cid AND business_id = :bid"
                        ),
                        {
                            "call_id": refid,
                            "u": now_app(),
                            "id": int(contact_id),
                            "cid": int(campaign_id),
                            "bid": int(business_id),
                        },
                    )

                payload: dict[str, Any] = {
                    "to": number,
                    "agent_name": agent_name,
                    "call_id": refid,
                }
                # Default from backend/.env (loaded by campaign_port Settings), unless caller overrides.
                if callback_url:
                    payload["callback_url"] = callback_url
                else:
                    try:
                        payload["callback_url"] = build_mcube_callback_url(settings)
                    except Exception:
                        pass
                if refurl:
                    payload["refurl"] = refurl
                else:
                    try:
                        payload["refurl"] = build_mcube_refurl_for_call(refid, settings)
                    except Exception:
                        pass

                resp = await client.post(settings.mcube_local_outbound_url, json=payload)
                resp_json: dict[str, Any] = {}
                try:
                    resp_json = resp.json()
                except Exception:
                    resp_json = {"_raw": (resp.text or "")[:500]}

                ok = bool(resp_json.get("ok")) if isinstance(resp_json, dict) else False
                mcube_sid = ""
                status_txt = ""
                if isinstance(resp_json, dict):
                    mcube_sid = str(resp_json.get("mcube_call_sid") or resp_json.get("call_sid") or "").strip()
                    status_txt = str(resp_json.get("status") or "").strip()

                with session_scope_cluster() as db:
                    if ok:
                        # Prefer MCube call sid when present; otherwise keep our refid.
                        db.execute(
                            text(
                                f"UPDATE `{contacts_table}` "
                                "SET status = 'initiated', call_id = :call_id, dialstatus = :ds, updated_at = :u "
                                "WHERE id = :id AND campaign_id = :cid AND business_id = :bid"
                            ),
                            {
                                "call_id": mcube_sid or refid,
                                "ds": status_txt or "initiated",
                                "u": now_app(),
                                "id": int(contact_id),
                                "cid": int(campaign_id),
                                "bid": int(business_id),
                            },
                        )
                    else:
                        preview = (
                            (resp_json.get("error") if isinstance(resp_json, dict) else None)
                            or (resp_json.get("_raw") if isinstance(resp_json, dict) else None)
                            or f"http_{resp.status_code}"
                        )
                        db.execute(
                            text(
                                f"UPDATE `{contacts_table}` "
                                "SET status = 'failed', dialstatus = :ds, updated_at = :u "
                                "WHERE id = :id AND campaign_id = :cid AND business_id = :bid"
                            ),
                            {
                                "ds": str(preview)[:250],
                                "u": now_app(),
                                "id": int(contact_id),
                                "cid": int(campaign_id),
                                "bid": int(business_id),
                            },
                        )
            except Exception as e:  # noqa: BLE001
                logger.error("auto-call failed contact_id=%s err=%s", contact_id, e)

            # Delay between trigger calls (including after the last one is fine).
            if i < len(contact_ids) - 1:
                await asyncio.sleep(delay_s)


@router.post("", status_code=201)
def store_campaign(
    request: Request,
    body: CampaignCreateBody,
    user: Annotated[AuthUser, Depends(get_current_user)],
    db_default: Session = Depends(get_db_default),
    db_cluster: Session = Depends(get_db_cluster),
):
    run_now = _php_filter_bool(body.run_now)
    if run_now and not body.start_time:
        raise HTTPException(
            status_code=422,
            detail={"message": "Validation failed", "errors": {"start_time": ["required when scheduled"]}},
        )

    business_id = user.business_id
    table_name = campaigns_table(business_id)
    if not has_table(db_cluster, table_name):
        raise HTTPException(
            status_code=500,
            detail={
                "message": "Campaign table not found. Please contact administrator.",
                "error": "Table does not exist",
            },
        )

    try:
        if run_now:
            start = date_parser.parse(body.start_time) if body.start_time else now_app()
            if start.tzinfo is None:
                start = start.replace(tzinfo=now_app().tzinfo)
            end = start + timedelta(days=365 * 2)
        else:
            start = now_app()
            end = start + timedelta(days=365 * 2)

        executive_number = ""
        if body.exeuctive_number:
            executive_number = fetch_executive_number(
                db_default, business_id, body.exeuctive_number, body.bot_name
            )

        callback_enabled = _php_filter_bool(body.callback_enabled)
        retry_time = 1 if callback_enabled else 0
        retry_logic = body.retry_logic or ""
        waiting_time_values = compute_waiting_time_for_new_campaign(
            db_default, business_id, retry_logic, body.custom_attempt_values
        )

        insert: dict[str, Any] = {
            "business_id": business_id,
            "name": body.name,
            "description": body.description,
            "status": "scheduled" if run_now else "active",
            "total_count": 0,
            "waiting_time": waiting_time_values or "",
            "did": executive_number,
            "start_time": format_dt(start),
            "end_time": format_dt(end),
            "start_date": start.strftime("%Y-%m-%d"),
            "end_date": end.strftime("%Y-%m-%d"),
            "retry_time": retry_time,
            "run_now": 1 if run_now else 0,
            "batch_size": 10,
            "batch_interval": 5,
            "created_at": now_app(),
            "updated_at": now_app(),
        }

        if has_column(db_cluster, table_name, "bot_id"):
            insert["bot_id"] = body.bot_name
        if has_column(db_cluster, table_name, "exeuctive_number"):
            insert["exeuctive_number"] = body.exeuctive_number
        if has_column(db_cluster, table_name, "retry_logic"):
            insert["retry_logic"] = retry_logic

        cols = ", ".join(f"`{k}`" for k in insert.keys())
        placeholders = ", ".join(f":{k}" for k in insert.keys())
        db_cluster.execute(text(f"INSERT INTO `{table_name}` ({cols}) VALUES ({placeholders})"), insert)
        campaign_id = int(db_cluster.execute(text("SELECT LAST_INSERT_ID()")).scalar_one())
        db_cluster.commit()

        campaign = db_cluster.execute(
            text(f"SELECT * FROM `{table_name}` WHERE campaign_id = :cid LIMIT 1"),
            {"cid": campaign_id},
        ).mappings().first()

        if run_now:
            try:
                db_default.execute(
                    text(
                        """
                        INSERT INTO scheduled_campaigns
                        (campaign_id, business_id, start_time, end_time, execute_at, delay_seconds, status, error_message, created_at, updated_at)
                        VALUES (:campaign_id, :business_id, :start_time, :end_time, :execute_at, 0, 'pending', NULL, :c_at, :u_at)
                        """
                    ),
                    {
                        "campaign_id": campaign_id,
                        "business_id": business_id,
                        "start_time": format_dt(start),
                        "end_time": format_dt(end),
                        "execute_at": format_dt(start),
                        "c_at": format_dt(now_app()),
                        "u_at": format_dt(now_app()),
                    },
                )
                db_default.commit()
            except Exception as e:  # noqa: BLE001
                logger.error("scheduled_campaigns insert failed: %s", e)
                db_default.rollback()

        username = user.name or user.email or "Unknown"
        audit_trail(
            {
                "business_id": business_id,
                "user_id": user.user_id,
                "user_name": username,
                "action": "create",
                "module": "Campaigns",
                "table_name": table_name,
                "resource_id": campaign_id,
                "resource_name": campaign.get("name") if campaign else None,
                "modified": f"Campaign created successfully by {username} from {request.client.host if request.client else ''} at {format_dt(now_app())}",
                "details": {
                    "campaign_id": campaign_id,
                    "campaign_name": campaign.get("name") if campaign else None,
                    "status": campaign.get("status") if campaign else None,
                    "run_now": run_now,
                },
            }
        )

        out = dict(campaign) if campaign else {}
        out["id"] = campaign_id
        return {
            "message": "Campaign created successfully",
            "campaign": out,
            "campaign_id": campaign_id,
            "id": campaign_id,
        }
    except HTTPException:
        raise
    except Exception as e:  # noqa: BLE001
        logger.error("Campaign creation error: %s", e)
        raise HTTPException(
            status_code=500,
            detail={"message": "Failed to create campaign", "error": str(e), "success": False},
        ) from e


@router.post("/{campaign_id}/contacts/upload")
async def upload_contacts(
    campaign_id: str,
    user: Annotated[AuthUser, Depends(get_current_user)],
    request: Request,
    file: UploadFile = File(...),
    db_default: Session = Depends(get_db_default),
    db_cluster: Session = Depends(get_db_cluster),
):
    business_id = user.business_id
    table_name = campaigns_table(business_id)

    fname = (file.filename or "").lower()
    if not fname.endswith((".csv", ".txt", ".xlsx")):
        raise HTTPException(
            status_code=422,
            detail={"message": "Validation failed", "errors": {"file": ["must be csv, txt, or xlsx"]}},
        )

    campaign = db_cluster.execute(
        text(f"SELECT * FROM `{table_name}` WHERE campaign_id = :cid LIMIT 1"),
        {"cid": campaign_id},
    ).mappings().first()
    if not campaign:
        raise HTTPException(status_code=404, detail={"message": "Campaign not found"})

    raw = await file.read()
    if len(raw) > 10 * 1024 * 1024:
        raise HTTPException(status_code=422, detail={"message": "File too large (max 10MB)"})
    if fname.endswith(".xlsx"):
        parse_result = workbook_bytes_to_contact_rows(raw)
    else:
        parse_result = parse_csv_content(raw.decode("utf-8", errors="replace"))

    if not parse_result.get("success"):
        raise HTTPException(status_code=400, detail={"message": parse_result.get("error"), "success": False})

    try:
        store_result = campaign_core.store_campaign_contacts(
            db_cluster,
            db_default,
            business_id,
            int(campaign_id),
            parse_result["contacts"],
        )
        db_cluster.commit()
        inserted_contacts = store_result.get("contacts") or []

        current = db_cluster.execute(
            text(f"SELECT total_count FROM `{table_name}` WHERE campaign_id = :cid LIMIT 1"),
            {"cid": campaign_id},
        ).first()
        current_total = int(current[0] or 0) if current else 0
        new_count = int(parse_result["total_count"])
        updated_total = current_total + new_count
        db_cluster.execute(
            text(f"UPDATE `{table_name}` SET total_count = :tc WHERE campaign_id = :cid"),
            {"tc": updated_total, "cid": campaign_id},
        )
        db_cluster.commit()

        campaign = db_cluster.execute(
            text(f"SELECT * FROM `{table_name}` WHERE campaign_id = :cid LIMIT 1"),
            {"cid": campaign_id},
        ).mappings().first()

        call_result = None
        if campaign and int(campaign.get("run_now") or 0) == 0:
            minutes_check = check_remaining_minutes(db_default, business_id)
            if not minutes_check["valid"]:
                raise HTTPException(
                    status_code=400,
                    detail={
                        "message": minutes_check["message"],
                        "success": False,
                        "insufficient_minutes": True,
                        "remain_min": minutes_check.get("remain_min", 0),
                        "contacts_imported": new_count,
                        "total_count": updated_total,
                    },
                )
            btc = check_business_time_window(db_default, business_id)
            if not btc["valid"]:
                raise HTTPException(
                    status_code=400,
                    detail={
                        "message": btc["message"],
                        "success": False,
                        "error_code": "BUSINESS_TIME_BLOCKED",
                        "business_time": btc.get("business_time"),
                        "current_time": btc.get("current_time"),
                        "current_day": btc.get("current_day"),
                        "contacts_imported": new_count,
                        "total_count": updated_total,
                    },
                )

            if campaign.get("status") not in ("active", "processing"):
                db_cluster.execute(
                    text(f"UPDATE `{table_name}` SET status = 'active', updated_at = :u WHERE campaign_id = :cid"),
                    {"u": now_app(), "cid": campaign_id},
                )
                db_cluster.commit()

            try:
                # Auto-call immediately after upload (sequential, 2s delay by default).
                # Inputs:
                # - agent_name: campaign.bot_id if present, else "default"
                # - callback_url/refurl: optional (can be provided by client via headers)
                agent_name = str(campaign.get("bot_id") or "default").strip() or "default"
                delay_seconds = 2.0  # explicit default: 2 seconds

                callback_url = (request.headers.get("x-mcube-callback-url") or "").strip() or None
                refurl = (request.headers.get("x-mcube-refurl") or "").strip() or None

                contact_ids = [int(c.get("id")) for c in inserted_contacts if c.get("id") is not None]
                asyncio.create_task(
                    _auto_call_contacts_after_upload(
                        business_id=int(business_id),
                        campaign_id=int(campaign_id),
                        contact_ids=contact_ids,
                        agent_name=agent_name,
                        callback_url=callback_url,
                        refurl=refurl,
                        delay_seconds=delay_seconds,
                    )
                )
                call_result = {
                    "success": True,
                    "started": True,
                    "message": f"Calling started for {len(contact_ids)} contacts (2s delay).",
                    "agent_name": agent_name,
                }
            except Exception as e:  # noqa: BLE001
                logger.error("auto-call start failed: %s", e)
                call_result = {"success": False, "error": f"Failed to start calling: {e}"}
        else:
            call_result = {
                "success": True,
                "message": f"Campaign scheduled for {campaign.get('start_time') if campaign else 'N/A'}",
                "scheduled": True,
                "cron_scheduled": True,
            }

        return {
            "message": parse_result.get("message"),
            "contacts_imported": new_count,
            "total_count": updated_total,
            "previous_count": current_total,
            "contacts_data": parse_result.get("contacts"),
            "inserted_contacts": inserted_contacts,
            "campaign_id": campaign_id,
            "success": True,
            "call_result": call_result,
        }
    except HTTPException:
        raise
    except Exception as e:  # noqa: BLE001
        logger.error("upload error: %s", e)
        raise HTTPException(
            status_code=500,
            detail={"message": f"Failed to upload contacts: {e}", "success": False},
        ) from e


@router.post("/{campaign_id}/start")
def start_campaign(
    campaign_id: str,
    user: Annotated[AuthUser, Depends(get_current_user)],
    db_default: Session = Depends(get_db_default),
    db_cluster: Session = Depends(get_db_cluster),
):
    business_id = user.business_id
    table_name = campaigns_table(business_id)
    if not has_table(db_cluster, table_name):
        raise HTTPException(status_code=404, detail={"message": "Campaign not found"})

    campaign = db_cluster.execute(
        text(f"SELECT * FROM `{table_name}` WHERE campaign_id = :cid LIMIT 1"),
        {"cid": campaign_id},
    ).mappings().first()
    if not campaign:
        raise HTTPException(status_code=404, detail={"message": "Campaign not found"})

    btc = check_business_time_window(db_default, business_id)
    if not btc["valid"]:
        raise HTTPException(
            status_code=400,
            detail={
                "message": btc["message"],
                "success": False,
                "error_code": "BUSINESS_TIME_BLOCKED",
                "business_time": btc.get("business_time"),
                "current_time": btc.get("current_time"),
                "current_day": btc.get("current_day"),
            },
        )

    minutes_check = check_remaining_minutes(db_default, business_id)
    if not minutes_check["valid"]:
        raise HTTPException(
            status_code=400,
            detail={
                "message": minutes_check["message"],
                "success": False,
                "insufficient_minutes": True,
                "remain_min": minutes_check.get("remain_min", 0),
            },
        )

    if campaign.get("status") not in ("active", "processing", "paused"):
        raise HTTPException(
            status_code=400,
            detail={
                "message": f"Campaign can only be started from active, processing, or paused status. Current status: {campaign.get('status')}",
                "success": False,
                "current_status": campaign.get("status"),
            },
        )

    contacts_table = campaign_contacts_table(business_id)
    pending = 0
    if has_table(db_cluster, contacts_table):
        pending = int(
            db_cluster.execute(
                text(
                    f"SELECT COUNT(*) FROM `{contacts_table}` WHERE campaign_id = :cid "
                    "AND (status = 'pending' OR status IS NULL)"
                ),
                {"cid": campaign_id},
            ).scalar_one()
        )

    if pending == 0 and campaign.get("status") == "paused":
        db_cluster.execute(
            text(f"UPDATE `{table_name}` SET status = 'completed', updated_at = :u WHERE campaign_id = :cid"),
            {"u": now_app(), "cid": campaign_id},
        )
        db_cluster.commit()
        from app.utils.rabbitmq_campaign import delete_queue_for_campaign

        delete_queue_for_campaign(business_id, int(campaign_id))
        return {
            "message": "Campaign has no pending contacts. Marked as completed.",
            "success": True,
            "completed": True,
        }

    db_cluster.execute(
        text(f"UPDATE `{table_name}` SET status = 'active', updated_at = :u WHERE campaign_id = :cid"),
        {"u": now_app(), "cid": campaign_id},
    )
    db_cluster.commit()

    try:
        dispatch_campaign_to_queue(
            db_default,
            int(campaign_id),
            business_id,
            False,
            False,
            get_settings().queue_first_campaign,
        )
        msg = "Campaign resumed successfully" if campaign.get("status") == "paused" else "Campaign started successfully"
        return {
            "message": msg,
            "success": True,
            "queued": True,
            "message_detail": "Campaign execution has been queued and will start processing in the background. You can continue using the application while calls are being made.",
        }
    except Exception as e:  # noqa: BLE001
        logger.error("dispatch error: %s", e)
        raise HTTPException(
            status_code=500,
            detail={
                "message": "Failed to queue campaign job. Please ensure queue workers are running.",
                "success": False,
                "error": str(e),
                "message_detail": f"Error: {e}. Please check queue configuration and ensure queue workers are running.",
            },
        ) from e
