"""
Shared execution pipeline.

Both entry points run through here so they cannot drift apart:
  - POST /api/ai/execute  → capability/bot resolved by core/intent.py
  - POST /<agent-slug>    → capability/bot resolved by core/agent_catalog.py

Everything after the (initiative, capability) pair is decided is identical:
prompt/model resolution, validation, execution logging, dispatch.
"""
import json as _json
import logging
from dataclasses import dataclass
from typing import Any, Optional

from fastapi import HTTPException
from sqlalchemy.orm import Session
from starlette.datastructures import UploadFile as StarletteUploadFile

from enterprise_ai.core.dispatcher import dispatch
from enterprise_ai.core.service_config import ServiceNotConfigured
from enterprise_ai.models.bot import MAIbot
from enterprise_ai.models.capability import MAIcapability
from enterprise_ai.models.execution import TAIexecution
from enterprise_ai.models.initiative import MAIinitiative
from enterprise_ai.services.config_resolver import get_active_config
from enterprise_ai.services.model_resolver import get_model
from enterprise_ai.services.prompt_resolver import get_prompt

logger = logging.getLogger(__name__)

MAX_FILE_SIZE = 50 * 1024 * 1024  # 50 MB
ALLOWED_CONTENT_TYPES = {
    "application/pdf",
    "image/jpeg",
    "image/png",
    "image/gif",
    "text/plain",
    "text/csv",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "application/msword",
    "audio/mpeg",
    "audio/wav",
    "audio/ogg",
}


@dataclass
class ParsedRequest:
    input_text: Optional[str]
    session_id: Optional[str]
    bot_hint: Optional[str]
    capability: Optional[str]
    login: Optional[str]
    file: Any
    input_data: dict


def _normalize_login(login):
    """GoodBooks login JSON is forwarded as a header — keep it ASCII and compact."""
    if not login:
        return None
    try:
        return _json.dumps(_json.loads(login), ensure_ascii=True, separators=(',', ':'))
    except (ValueError, TypeError):
        return login.encode('ascii', errors='ignore').decode('ascii')


def _attach_context(input_data: dict, tenant_id: int, user_id: int, session_id, login):
    """Internal context every adapter receives; adapters strip it before sending."""
    if session_id:
        input_data["session_id"] = session_id
    input_data["_tenant_id"] = tenant_id
    input_data["_user_id"] = user_id
    if login:
        input_data["_login"] = login


async def _parse_json_body(request, tenant_id: int, user_id: int) -> ParsedRequest:
    """
    application/json path — the body itself is the payload.

    Out-of-band fields have their own channels so they never pollute what gets
    forwarded downstream: `capability` and `bot_hint` from the query string,
    `login` from the Login header. `session_id` may be a key in the body.
    """
    try:
        body = await request.json()
    except Exception:
        raise HTTPException(status_code=400, detail="Malformed JSON body")

    if body is None or body == {} or body == []:
        raise HTTPException(status_code=400, detail="JSON body is empty")

    serialized = _json.dumps(body, separators=(",", ":"))

    if isinstance(body, dict):
        input_data = dict(body)
        session_id = input_data.get("session_id") or None
    else:
        # Arrays and scalars ride as text, matching the form path. Adapters that
        # expect a list (tax_suggestions) unwrap it again.
        input_data = {"text": serialized}
        session_id = None

    login = _normalize_login(request.headers.get("Login"))
    _attach_context(input_data, tenant_id, user_id, session_id, login)

    return ParsedRequest(
        input_text=serialized,
        session_id=session_id,
        bot_hint=request.query_params.get("bot_hint") or None,
        capability=request.query_params.get("capability") or None,
        login=login,
        file=None,
        input_data=input_data,
    )


async def parse_agent_form(request, tenant_id: int, user_id: int, *, hint: str = None) -> ParsedRequest:
    """
    Build input_data from the request, whichever way the caller sent it.

    Two content types are accepted:
      * application/json   — the body IS the payload (natural for JSON agents)
      * multipart/form-data / urlencoded — fields `input`, `file`, `session_id`,
        `bot_hint`, `capability`, `login`

    `hint` selects the payload shape on the form path. On /api/ai/execute it is
    the bot_hint field; on a named agent route it is the slug — so POST /chatbot
    and POST /api/ai/execute with bot_hint=chatbot produce identical input_data.
    """
    content_type = (request.headers.get("content-type") or "").split(";")[0].strip().lower()

    if content_type == "application/json":
        return await _parse_json_body(request, tenant_id, user_id)

    form = await request.form()
    input_text = form.get("input") or None
    session_id = form.get("session_id") or None
    bot_hint = form.get("bot_hint") or None
    capability = form.get("capability") or request.query_params.get("capability") or None
    login = _normalize_login(form.get("login"))

    raw_file = form.get("file")
    file = raw_file if isinstance(raw_file, StarletteUploadFile) and raw_file.filename else None

    effective_hint = hint or bot_hint

    if not input_text and not file:
        raise HTTPException(
            status_code=400,
            detail=(
                "Provide either input text or a file. Send multipart/form-data with an "
                "'input' field (or 'file'), or POST the payload directly with "
                "Content-Type: application/json."
            ),
        )

    # Validate uploaded file
    if file is not None:
        if file.content_type and file.content_type not in ALLOWED_CONTENT_TYPES:
            raise HTTPException(status_code=400, detail="File type not allowed")
        contents = await file.read()
        if len(contents) > MAX_FILE_SIZE:
            raise HTTPException(status_code=400, detail="File too large (max 50MB)")
        await file.seek(0)

    # Auto-detect input_type and build input_data
    if file is not None:
        input_type = "voice" if (file.content_type and file.content_type.startswith("audio/")) else "file"
        input_data = {"input_type": input_type}
    else:
        if effective_hint == "chatbot":
            input_data = {"message": input_text}
        elif effective_hint == "generate_profile":
            # Accept JSON dict OR plain pipe-separated string
            try:
                parsed = _json.loads(input_text)
                input_data = parsed if isinstance(parsed, dict) else {"text": input_text}
            except (ValueError, TypeError):
                input_data = {"text": input_text}
        else:
            try:
                parsed = _json.loads(input_text)
                input_data = parsed if isinstance(parsed, dict) else {"text": input_text}
            except (ValueError, TypeError):
                if session_id:
                    input_data = {"message": input_text}
                else:
                    input_data = {"text": input_text}

    _attach_context(input_data, tenant_id, user_id, session_id, login)

    return ParsedRequest(
        input_text=input_text,
        session_id=session_id,
        bot_hint=bot_hint,
        capability=capability,
        login=login,
        file=file,
        input_data=input_data,
    )


def resolve_bot_for_initiative(db: Session, tenant_id: int, initiative_id: int):
    """Active read-only bot for an initiative — used by the named agent routes."""
    return db.query(MAIbot).filter(
        MAIbot.AIinitiativeid == initiative_id,
        MAIbot.TENANTID == tenant_id,
        MAIbot.isreadonly == 1
    ).first()


def run_execution(
    db: Session,
    tenant_id: int,
    user_id: int,
    initiative_code: str,
    capability_code: str,
    input_data: dict,
    file=None,
    bot_code: str = None,
):
    """
    Resolve config, validate, log, dispatch, and record the outcome.

    Returns (execution, result). Raises HTTPException on any validation failure.
    """
    prompt_template = get_prompt(db, initiative_code, capability_code, tenant_id)
    model_config = get_model(db, tenant_id)

    # Active config version overrides DB prompt/model when present
    active_config = get_active_config(db, initiative_code)
    if active_config:
        if active_config.prompt:
            prompt_template = active_config.prompt
        if active_config.model:
            model_config = {
                "model": active_config.model,
                "temperature": active_config.temperature if active_config.temperature is not None else 0.7,
            }

    if not prompt_template:
        raise HTTPException(status_code=404, detail="Prompt template not configured")

    if not model_config:
        raise HTTPException(status_code=404, detail="Model not configured")

    # 1️⃣ Validate Initiative
    initiative = db.query(MAIinitiative).filter(
        MAIinitiative.aiinitiativecode == initiative_code,
        MAIinitiative.tenantid == tenant_id
    ).first()

    if not initiative:
        raise HTTPException(status_code=400, detail="Invalid Initiative")

    # 2️⃣ Validate Capability
    capability = db.query(MAIcapability).filter(
        MAIcapability.AIcapabilitycode == capability_code,
        MAIcapability.TENANTID == tenant_id
    ).first()

    if not capability:
        raise HTTPException(status_code=400, detail="Invalid Capability")

    # 3️⃣ Validate Bot — explicit code from intent resolution, else the
    #    initiative's active read-only bot (named agent routes)
    if bot_code:
        bot = db.query(MAIbot).filter(
            MAIbot.AIbotcode == bot_code,
            MAIbot.AIinitiativeid == initiative.aiinitiativeid,
            MAIbot.TENANTID == tenant_id
        ).first()
    else:
        bot = resolve_bot_for_initiative(db, tenant_id, initiative.aiinitiativeid)

    if not bot:
        raise HTTPException(status_code=400, detail="Invalid Bot")

    if bot.isreadonly != 1:
        raise HTTPException(status_code=403, detail="Bot is not read-only")

    # 4️⃣ Create Execution Log
    execution = TAIexecution(
        userid=user_id,
        tenantid=tenant_id,
        AIinitiativeid=initiative.aiinitiativeid,
        AIbotid=bot.AIbotid,
        AIcapabilityid=capability.AIcapabilityid,
        outcomestatus=0
    )

    try:
        db.add(execution)
        db.commit()
        db.refresh(execution)
    except Exception:
        db.rollback()
        raise HTTPException(status_code=500, detail="Failed to create execution record")

    try:
        # 🔥 Call Service
        result = dispatch(
            initiative_code,
            capability_code,
            input_data,
            file,
            prompt_template,
            model_config
        )

        # 6️⃣ Save Input & Output
        execution.inputpayload = input_data
        execution.outputpayload = result
        execution.sessionid = input_data.get("session_id")
        execution.outcomestatus = 1

        db.commit()
        db.refresh(execution)

    except ServiceNotConfigured as e:
        # Deployment gap, not a runtime fault — name the variable to set.
        logger.error("Agent '%s' is not configured: %s", initiative_code, e)
        execution.outcomestatus = -1
        execution.outputpayload = {"error": "not_configured", "detail": str(e)}
        db.commit()
        raise HTTPException(status_code=503, detail=str(e))

    except Exception as e:
        logger.error("AI execution failed: %s", str(e), exc_info=True)
        execution.outcomestatus = -1
        execution.outputpayload = {"error": "execution_failed"}
        db.commit()
        raise HTTPException(status_code=500, detail="AI execution failed")

    return execution, result
