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

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

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)

    # 2. Build delivery hint + job memory hint
    delivery_hint = _build_delivery_hint(job.delivery)
    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. Assemble system prompt
    system = assemble_agent_prompt(
        soul=resources.soul,
        user_soul=resources.user_soul,
        skills=resources.skill_info if resources.skill_info else None,
        mode="autonomous",
        memory_context=resources.memory_context,
        user_profile=resources.user_profile,
        fliiq_email=resources.fliiq_email,
    )

    # 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 ""
    except Exception as e:
        log.error("job_agent_loop_failed", job=job.name, error=str(e))
        status = "error"
        error = str(e)
        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))

    # 8. 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) -> 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":
        return (
            f"DELIVERY: After completing this task, send the results via Telegram.\n"
            f"Use the send_telegram tool with chat_id=\"{delivery.to}\".\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"


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,
    )
