"""Job executor — runs a job through agent_loop() and saves results."""

import os
import re
from datetime import datetime, timezone
from pathlib import Path

import httpx
import structlog

from fliiq.runtime.agent.config import AgentConfig
from fliiq.runtime.agent.loop import agent_loop
from fliiq.runtime.agent.prompt import assemble_agent_prompt
from fliiq.runtime.agent.setup import setup_agent_resources
from fliiq.runtime.llm.providers import LLMConfig
from fliiq.runtime.scheduler.loader import update_job_state
from fliiq.runtime.scheduler.models import DeliveryConfig, JobDefinition, JobState, RunLogEntry
from fliiq.runtime.scheduler.run_log import save_run_log

log = structlog.get_logger()

_TEMPLATE_RE = re.compile(r"\{\{(\w+(?:\.\w+)*)\}\}")


def template_prompt(prompt: str, payload: dict | None) -> str:
    """Replace {{field.nested}} placeholders with values from payload dict.

    Missing keys are left as-is. Payload values are wrapped in
    <external_message> tags to prevent prompt injection.
    """
    if not payload:
        return prompt

    def _replace(match: re.Match) -> str:
        key_path = match.group(1).split(".")
        value = payload
        for key in key_path:
            if isinstance(value, dict) and key in value:
                value = value[key]
            else:
                return match.group(0)  # Leave placeholder as-is
        return (
            f"<external_message source=\"webhook\">\n"
            f"{value}\n"
            f"</external_message>"
        )

    return _TEMPLATE_RE.sub(_replace, prompt)


async def execute_job(
    job: JobDefinition,
    jobs_dir: Path,
    project_root: Path,
    llm_config: LLMConfig,
    payload: dict | None = None,
) -> RunLogEntry:
    """Execute a single job: setup resources -> agent_loop() -> save state + run log."""
    started_at = datetime.now(timezone.utc)
    log.info("job_executing", job=job.name, trigger=job.trigger.type)

    # 1. Template prompt with webhook payload if any
    prompt = template_prompt(job.prompt, payload)

    # 1.5 Resolve Telegram Forum Topic if delivery.channel is set
    message_thread_id = None
    if (
        job.delivery
        and job.delivery.type == "telegram"
        and job.delivery.channel
        and job.delivery.to
    ):
        from fliiq.runtime.telegram.topics import resolve_or_create_topic

        message_thread_id = await resolve_or_create_topic(
            job.delivery.to, job.delivery.channel,
        )

    # 2. Build delivery hint + job memory hint
    delivery_hint = _build_delivery_hint(job.delivery, message_thread_id)
    memory_file = project_root / ".fliiq" / "memory" / "jobs" / f"{job.name}.md"
    job_prompt = (
        f"{prompt}\n\n"
        f"{delivery_hint}"
        f"Your job memory file is: {memory_file}\n"
        f"Use read_file to check previous entries, and write_file to append your output with today's date.\n"
        f"If the file doesn't exist yet (first run), create it."
    )

    # 3. Setup agent resources (no interactive handlers — jobs run headless)
    def _noop_ask_user(question: str) -> str:
        return "Not available — this is a background job with no interactive user."

    def _noop_ask_user_choice(question: str, options: list[dict], recommended: int) -> str:
        if options and recommended and 1 <= recommended <= len(options):
            picked = options[recommended - 1]
            return f"Auto-selected: {picked['label']} (no interactive user)"
        return "Auto-selected first option (no interactive user)"

    try:
        resources = await setup_agent_resources(
            mode="autonomous",
            llm_config=llm_config,
            project_root=project_root,
            ask_user_handler=_noop_ask_user,
            ask_user_choice_handler=_noop_ask_user_choice,
        )
    except Exception as e:
        log.error("job_setup_failed", job=job.name, error=str(e))
        return _error_entry(job.name, started_at, f"Setup failed: {e}")

    # 4. Enrich memory with prompt-relevant entities + assemble system prompt
    from fliiq.runtime.agent.setup import (
        _load_knowledge_context,
        enrich_knowledge_context,
        enrich_memory_context,
    )

    enriched_memory = enrich_memory_context(resources.memory_context, prompt, project_root)
    knowledge_context = _load_knowledge_context()
    enriched_knowledge = enrich_knowledge_context(knowledge_context, prompt)

    # Load playbook if job specifies one
    playbook_content = None
    if job.playbook:
        from fliiq.runtime.planning.playbook_loader import load_playbook

        playbook_content = load_playbook(job.playbook, project_root)

    system = assemble_agent_prompt(
        soul=resources.soul,
        user_soul=resources.user_soul,
        playbook=playbook_content,
        skills=resources.skill_info if resources.skill_info else None,
        mode="autonomous",
        memory_context=enriched_memory,
        user_profile=resources.user_profile,
        fliiq_email=resources.fliiq_email,
        knowledge_context=enriched_knowledge,
    )

    # 5. Run agent loop
    config = AgentConfig(mode="autonomous")
    messages = [{"role": "user", "content": job_prompt}]

    response_text = ""
    try:
        result = await agent_loop(
            llm=resources.llm,
            messages=messages,
            tools=resources.tools,
            config=config,
            system=system,
        )
        status = "success"
        error = None
        iterations = result.iterations
        stop_reason = result.stop_reason
        response_text = result.final_text or ""

        # Post-conversation memory extraction (non-fatal)
        if result.iterations > 0:
            try:
                from fliiq.runtime.memory.extractor import extract_and_save_memories

                await extract_and_save_memories(resources.llm, result.messages, project_root)
            except Exception:
                pass
    except Exception as e:
        error_detail = f"{type(e).__name__}: {e!r}"
        log.error("job_agent_loop_failed", job=job.name, error=error_detail, exc_type=type(e).__name__)
        status = "error"
        error = error_detail
        iterations = 0
        stop_reason = "error"

    completed_at = datetime.now(timezone.utc)
    duration_ms = int((completed_at - started_at).total_seconds() * 1000)

    # 6. Update job state
    try:
        new_state = JobState(
            last_run_at=completed_at,
            last_status=status,
            run_count=job.state.run_count + 1,
        )
        update_job_state(job.name, jobs_dir, new_state)
    except Exception as e:
        log.warning("job_state_update_failed", job=job.name, error=str(e))

    # 7. Save run log
    entry = RunLogEntry(
        job_name=job.name,
        started_at=started_at,
        completed_at=completed_at,
        status=status,
        duration_ms=duration_ms,
        error=error,
        iterations=iterations,
        stop_reason=stop_reason,
        response_text=response_text,
    )
    try:
        save_run_log(entry, jobs_dir)
    except Exception as e:
        log.warning("job_run_log_failed", job=job.name, error=str(e))

    # 7.5 Notify user of failure via delivery channel
    if status == "error" and job.delivery:
        await _send_error_notification(job, error, message_thread_id)

    # 8. Auto-cleanup one-time "at" jobs after successful fire
    if status == "success" and job.trigger.type == "at":
        try:
            job_file = jobs_dir / f"{job.name}.yaml"
            if job_file.is_file():
                job_file.unlink()
                log.info("at_job_autocleaned", job=job.name)
        except Exception as e:
            log.warning("at_job_autoclean_failed", job=job.name, error=str(e))

    # 9. Write response to job memory (guaranteed persistence)
    if response_text:
        try:
            memory_file = project_root / ".fliiq" / "memory" / "jobs" / f"{job.name}.md"
            memory_file.parent.mkdir(parents=True, exist_ok=True)
            header = f"\n\n---\n## {completed_at.strftime('%Y-%m-%d %H:%M')}\n\n"
            with open(memory_file, "a") as f:
                f.write(header + response_text + "\n")
        except Exception as e:
            log.warning("job_memory_write_failed", job=job.name, error=str(e))

    log.info("job_completed", job=job.name, status=status, duration_ms=duration_ms)
    return entry


def _build_delivery_hint(
    delivery: DeliveryConfig | None, message_thread_id: int | None = None,
) -> str:
    """Build delivery instruction text for the agent prompt."""
    if not delivery:
        return ""

    if delivery.type == "email":
        return (
            f"DELIVERY: After completing this task, send the results via email.\n"
            f"Use the send_email tool with to=\"{delivery.to}\".\n"
            f"Write a clear subject line and include your findings in the body.\n\n"
        )
    elif delivery.type == "sms":
        return (
            f"DELIVERY: After completing this task, send a brief summary via SMS.\n"
            f"Use the send_sms tool with to=\"{delivery.to}\".\n"
            f"Keep the message concise (under 300 chars for readability).\n\n"
        )
    elif delivery.type == "telegram":
        thread_part = ""
        if message_thread_id is not None:
            thread_part = f' message_thread_id="{message_thread_id}"'
        return (
            f"DELIVERY: After completing this task, send the results via Telegram.\n"
            f"Use the send_telegram tool with chat_id=\"{delivery.to}\"{thread_part}.\n"
            f"Format the message with Markdown for readability.\n\n"
        )
    else:
        return f"DELIVERY: Delivery type '{delivery.type}' is not yet supported. Skip delivery.\n\n"


async def _send_error_notification(
    job: JobDefinition, error: str | None, message_thread_id: int | None = None,
) -> None:
    """Best-effort: notify user of job failure via the job's delivery channel."""
    short_error = (error or "Unknown error")[:200]
    msg = f"Job '{job.name}' failed: {short_error}"

    try:
        if job.delivery.type == "telegram":
            bot_token = os.environ.get("TELEGRAM_BOT_TOKEN")
            if not bot_token:
                return
            payload: dict = {
                "chat_id": job.delivery.to,
                "text": msg,
            }
            if message_thread_id is not None:
                payload["message_thread_id"] = message_thread_id
            async with httpx.AsyncClient(timeout=15) as client:
                await client.post(
                    f"https://api.telegram.org/bot{bot_token}/sendMessage",
                    json=payload,
                )
        elif job.delivery.type == "email":
            from fliiq.runtime.google_auth import resolve_gmail_creds

            creds = resolve_gmail_creds()
            if not creds:
                return
            import smtplib
            from email.mime.text import MIMEText

            mime = MIMEText(msg)
            mime["Subject"] = f"Fliiq job failed: {job.name}"
            mime["To"] = job.delivery.to
            mime["From"] = creds["address"]
            with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
                smtp.login(creds["address"], creds["password"])
                smtp.send_message(mime)
    except Exception as e:
        log.warning("job_error_notification_failed", job=job.name, error=str(e))


def _error_entry(job_name: str, started_at: datetime, error: str) -> RunLogEntry:
    completed_at = datetime.now(timezone.utc)
    return RunLogEntry(
        job_name=job_name,
        started_at=started_at,
        completed_at=completed_at,
        status="error",
        duration_ms=int((completed_at - started_at).total_seconds() * 1000),
        error=error,
    )
