# @File: backend/services/whatsapp/workflows.py
# @Desc: Workflow automation service for the WhatsApp reseller module.
#        A "workflow" pairs a trigger (keyword match, first-message, no
#        reply after N minutes, etc.) with an ordered list of actions
#        (send a reply, tag the contact, mark as a lead, ping admin).
#
#        The service owns CRUD over ``WhatsappAutomationWorkflow`` and the
#        append-only ``WhatsappWorkflowRun`` audit table. It also exposes
#        ``match_triggers`` and ``execute_workflow`` for the incoming-
#        message pipeline to call. Those are written defensively so
#        unknown trigger/action types log a warning and are simply
#        skipped rather than crashing the message handler.
from __future__ import annotations

import json
import logging
from datetime import datetime, timezone
from typing import Any, Iterable, Optional

from fastapi import HTTPException, status
from models.whatsapp import (
    WhatsappAutomationWorkflow,
    WhatsappClientSubscription,
    WhatsappWorkflowRun,
)
from sqlalchemy import and_, desc, select
from sqlalchemy.ext.asyncio import AsyncSession

from services.whatsapp.chat import _get_active_subscription

logger = logging.getLogger(__name__)

WORKFLOW_LIST_LIMIT = 100
RUN_LIST_LIMIT = 200

SUPPORTED_TRIGGER_TYPES = {
    "new_message",
    "keyword",
    "first_message",
    "outside_hours",
    "no_reply_after",
    "added_to_campaign",
}
SUPPORTED_ACTION_TYPES = {
    "send_message",
    "assign_tag",
    "mark_lead",
    "notify_admin",
}


async def _require_subscription(db: AsyncSession, user_id: str) -> WhatsappClientSubscription:
    sub = await _get_active_subscription(db, user_id)
    if sub is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="No active WhatsApp subscription found for this account.",
        )
    return sub


def _parse_json(raw: Optional[str], default: Any) -> Any:
    if not raw:
        return default
    try:
        return json.loads(raw)
    except (TypeError, ValueError):
        logger.warning("Corrupted workflow JSON blob; falling back to default.")
        return default


def _normalize_trigger(trigger: dict | None) -> dict:
    """Clamp to supported types and drop unknown keys.

    Unknown types are preserved verbatim (so old rows still load) but the
    service's matcher ignores anything outside ``SUPPORTED_TRIGGER_TYPES``.
    """
    if not isinstance(trigger, dict):
        return {"type": "new_message"}
    out = {"type": str(trigger.get("type") or "new_message")}
    if trigger.get("keyword"):
        out["keyword"] = str(trigger["keyword"])[:128]
    if trigger.get("minutes") is not None:
        try:
            out["minutes"] = max(1, min(int(trigger["minutes"]), 10_080))
        except (TypeError, ValueError):
            pass
    if trigger.get("campaign_id") is not None:
        try:
            out["campaign_id"] = int(trigger["campaign_id"])
        except (TypeError, ValueError):
            pass
    return out


def _normalize_actions(actions: Iterable[dict] | None) -> list[dict]:
    cleaned: list[dict] = []
    for raw in actions or []:
        if not isinstance(raw, dict):
            continue
        atype = str(raw.get("type") or "").strip()
        if not atype:
            continue
        item: dict[str, Any] = {"type": atype}
        if raw.get("body"):
            item["body"] = str(raw["body"])[:4096]
        if raw.get("tag"):
            item["tag"] = str(raw["tag"])[:64]
        if raw.get("note"):
            item["note"] = str(raw["note"])[:512]
        cleaned.append(item)
    return cleaned


def _serialize(workflow: WhatsappAutomationWorkflow) -> dict:
    return {
        "id": workflow.id,
        "name": workflow.name,
        "description": workflow.description,
        "is_active": workflow.is_active,
        "trigger": _parse_json(workflow.trigger_json, {"type": "new_message"}),
        "actions": _parse_json(workflow.actions_json, []),
        "last_run_at": workflow.last_run_at,
        "run_count": workflow.run_count,
        "created_at": workflow.created_at,
        "updated_at": workflow.updated_at,
    }


def _serialize_run(run: WhatsappWorkflowRun) -> dict:
    return {
        "id": run.id,
        "workflow_id": run.workflow_id,
        "trigger_detail": run.trigger_detail,
        "result": run.result,
        "error_detail": run.error_detail,
        "executed_actions": run.executed_actions,
        "created_at": run.created_at,
    }


async def _load_workflow(
    db: AsyncSession, *, workflow_id: int, subscription_id: int
) -> WhatsappAutomationWorkflow:
    stmt = select(WhatsappAutomationWorkflow).where(
        and_(
            WhatsappAutomationWorkflow.id == workflow_id,
            WhatsappAutomationWorkflow.subscription_id == subscription_id,
        )
    )
    row = (await db.execute(stmt)).scalar_one_or_none()
    if row is None:
        raise HTTPException(status_code=404, detail="Workflow not found.")
    return row


async def list_workflows(db: AsyncSession, user_id: str) -> list[dict]:
    sub = await _require_subscription(db, user_id)
    stmt = (
        select(WhatsappAutomationWorkflow)
        .where(WhatsappAutomationWorkflow.subscription_id == sub.id)
        .order_by(desc(WhatsappAutomationWorkflow.updated_at))
        .limit(WORKFLOW_LIST_LIMIT)
    )
    rows = (await db.execute(stmt)).scalars().all()
    return [_serialize(r) for r in rows]


async def get_workflow(db: AsyncSession, user_id: str, workflow_id: int) -> dict:
    sub = await _require_subscription(db, user_id)
    row = await _load_workflow(db, workflow_id=workflow_id, subscription_id=sub.id)
    return _serialize(row)


async def create_workflow(db: AsyncSession, user_id: str, payload: dict) -> dict:
    sub = await _require_subscription(db, user_id)
    trigger = _normalize_trigger(payload.get("trigger"))
    actions = _normalize_actions(payload.get("actions"))
    if not actions:
        raise HTTPException(400, "A workflow needs at least one action.")
    if trigger["type"] not in SUPPORTED_TRIGGER_TYPES:
        raise HTTPException(400, f"Unsupported trigger type '{trigger['type']}'.")

    row = WhatsappAutomationWorkflow(
        subscription_id=sub.id,
        user_id=user_id,
        name=str(payload.get("name") or "").strip(),
        description=(payload.get("description") or None),
        is_active=bool(payload.get("is_active", True)),
        trigger_json=json.dumps(trigger, separators=(",", ":")),
        actions_json=json.dumps(actions, separators=(",", ":")),
    )
    db.add(row)
    await db.commit()
    await db.refresh(row)
    return _serialize(row)


async def update_workflow(
    db: AsyncSession, user_id: str, workflow_id: int, payload: dict
) -> dict:
    sub = await _require_subscription(db, user_id)
    row = await _load_workflow(db, workflow_id=workflow_id, subscription_id=sub.id)

    if "name" in payload and payload["name"] is not None:
        row.name = str(payload["name"]).strip()
    if "description" in payload:
        row.description = payload["description"] or None
    if "is_active" in payload and payload["is_active"] is not None:
        row.is_active = bool(payload["is_active"])
    if "trigger" in payload and payload["trigger"] is not None:
        trigger = _normalize_trigger(payload["trigger"])
        if trigger["type"] not in SUPPORTED_TRIGGER_TYPES:
            raise HTTPException(400, f"Unsupported trigger type '{trigger['type']}'.")
        row.trigger_json = json.dumps(trigger, separators=(",", ":"))
    if "actions" in payload and payload["actions"] is not None:
        actions = _normalize_actions(payload["actions"])
        if not actions:
            raise HTTPException(400, "A workflow needs at least one action.")
        row.actions_json = json.dumps(actions, separators=(",", ":"))

    await db.commit()
    await db.refresh(row)
    return _serialize(row)


async def delete_workflow(db: AsyncSession, user_id: str, workflow_id: int) -> None:
    sub = await _require_subscription(db, user_id)
    row = await _load_workflow(db, workflow_id=workflow_id, subscription_id=sub.id)
    await db.delete(row)
    await db.commit()


async def list_runs(db: AsyncSession, user_id: str, workflow_id: int) -> list[dict]:
    sub = await _require_subscription(db, user_id)
    await _load_workflow(db, workflow_id=workflow_id, subscription_id=sub.id)
    stmt = (
        select(WhatsappWorkflowRun)
        .where(WhatsappWorkflowRun.workflow_id == workflow_id)
        .order_by(desc(WhatsappWorkflowRun.created_at))
        .limit(RUN_LIST_LIMIT)
    )
    rows = (await db.execute(stmt)).scalars().all()
    return [_serialize_run(r) for r in rows]


# ---------------------------------------------------------------------------
# Trigger matching (used by the message handler, also powers dry-run)
# ---------------------------------------------------------------------------


def _keyword_matches(sample: str, keyword: Optional[str]) -> bool:
    if not keyword:
        return False
    return keyword.lower().strip() in sample.lower()


def match_trigger(trigger: dict, *, event: str, sample_message: str) -> bool:
    """Return ``True`` if ``trigger`` fires for the given event context.

    ``event`` is the coarse event classification ("new_message",
    "first_message", "outside_hours", "no_reply_after",
    "added_to_campaign"). ``sample_message`` is the raw inbound text, used
    for the keyword trigger.
    """
    ttype = trigger.get("type")
    if ttype not in SUPPORTED_TRIGGER_TYPES:
        return False
    if ttype == "new_message":
        return event == "new_message"
    if ttype == "keyword":
        return event == "new_message" and _keyword_matches(sample_message, trigger.get("keyword"))
    if ttype in {"first_message", "outside_hours", "no_reply_after", "added_to_campaign"}:
        return event == ttype
    return False


async def test_workflow(
    db: AsyncSession, user_id: str, workflow_id: int, payload: dict
) -> dict:
    """Dry-run a workflow against a sample message. No side effects."""
    sub = await _require_subscription(db, user_id)
    row = await _load_workflow(db, workflow_id=workflow_id, subscription_id=sub.id)
    trigger = _parse_json(row.trigger_json, {"type": "new_message"})
    actions = _parse_json(row.actions_json, [])
    sample = str((payload or {}).get("sample_message") or "").strip()
    event = str((payload or {}).get("context", {}).get("event") or "new_message")
    matched = match_trigger(trigger, event=event, sample_message=sample)

    preview_actions: list[dict] = []
    if matched:
        for act in actions:
            atype = act.get("type")
            if atype not in SUPPORTED_ACTION_TYPES:
                preview_actions.append({"type": atype, "status": "skipped", "reason": "unknown action"})
                continue
            preview_actions.append(
                {
                    "type": atype,
                    "status": "would_run",
                    "body": act.get("body"),
                    "tag": act.get("tag"),
                }
            )
    return {
        "matched": matched,
        "event": event,
        "trigger": trigger,
        "preview_actions": preview_actions,
    }


# ---------------------------------------------------------------------------
# Execution hook (called by the message handler; safe to call from here)
# ---------------------------------------------------------------------------


async def record_run(
    db: AsyncSession,
    workflow: WhatsappAutomationWorkflow,
    *,
    trigger_detail: str,
    result: str,
    executed_actions: int,
    error_detail: Optional[str] = None,
) -> None:
    """Persist a single workflow run audit row and bump counters.

    Kept small and synchronous-looking to avoid introducing a background
    task dependency — callers can fire-and-forget by wrapping in
    ``asyncio.create_task`` if needed.
    """
    db.add(
        WhatsappWorkflowRun(
            workflow_id=workflow.id,
            subscription_id=workflow.subscription_id,
            trigger_detail=trigger_detail[:1000] if trigger_detail else None,
            result=result,
            error_detail=error_detail[:1000] if error_detail else None,
            executed_actions=executed_actions,
        )
    )
    workflow.run_count = int(workflow.run_count or 0) + 1
    workflow.last_run_at = datetime.now(timezone.utc)
    await db.commit()