import asyncio
import os
from dataclasses import dataclass, field

from dotenv import load_dotenv
from typing_extensions import Never

from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler

from chat_with_pdf.build_index import create_faiss_index
from chat_with_pdf.constants import PDF_DIR, INDEX_DIR
from chat_with_pdf.download import download
from chat_with_pdf.find_context import find_context
from chat_with_pdf.qna import qna
from chat_with_pdf.rewrite_question import rewrite_question
from chat_with_pdf.utils.lock import acquire_lock

load_dotenv()


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------


@dataclass
class PdfChatInput:
    question: str
    pdf_url: str = "https://arxiv.org/pdf/1810.04805.pdf"
    chat_history: list = field(default_factory=list)
    config: dict = field(default_factory=lambda: {
        "EMBEDDING_MODEL_DEPLOYMENT_NAME": os.environ.get("EMBEDDING_MODEL_DEPLOYMENT_NAME", "text-embedding-ada-002"),
        "CHAT_MODEL_DEPLOYMENT_NAME": os.environ.get("CHAT_MODEL_DEPLOYMENT_NAME", "gpt-4"),
        "PROMPT_TOKEN_LIMIT": os.environ.get("PROMPT_TOKEN_LIMIT", "3000"),
        "MAX_COMPLETION_TOKENS": os.environ.get("MAX_COMPLETION_TOKENS", "1024"),
        "VERBOSE": os.environ.get("VERBOSE", "true"),
        "CHUNK_SIZE": os.environ.get("CHUNK_SIZE", "1024"),
        "CHUNK_OVERLAP": os.environ.get("CHUNK_OVERLAP", "64"),
    })


@dataclass
class BranchResult:
    index_path: str | None = None
    rewritten_question: str | None = None
    chat_history: list = field(default_factory=list)


# ---------------------------------------------------------------------------
# Executors
# ---------------------------------------------------------------------------


def _to_chatml(history: list) -> list[dict]:
    messages = []
    for item in history:
        messages.append({"role": "user", "content": item["inputs"]["question"]})
        messages.append({"role": "assistant", "content": item["outputs"]["answer"]})
    return messages


class InputExecutor(Executor):
    """Sets environment variables from config and creates working directories."""

    @handler
    async def receive(self, inp: PdfChatInput, ctx: WorkflowContext[PdfChatInput]) -> None:
        for key, value in inp.config.items():
            os.environ[key] = str(value)

        base_dir = os.path.join(os.path.dirname(__file__), "chat_with_pdf")
        with acquire_lock(os.path.join(base_dir, "create_folder.lock")):
            os.makedirs(PDF_DIR, exist_ok=True)
            os.makedirs(INDEX_DIR, exist_ok=True)

        await ctx.send_message(inp)


class IndexExecutor(Executor):
    """Downloads PDF and builds FAISS index (merges download_tool + build_index_tool)."""

    @handler
    async def process(self, inp: PdfChatInput, ctx: WorkflowContext[BranchResult]) -> None:
        pdf_path = download(inp.pdf_url)
        index_path = create_faiss_index(pdf_path)
        await ctx.send_message(BranchResult(index_path=index_path))


class RewriteExecutor(Executor):
    """Rewrites question using chat history for better context retrieval."""

    @handler
    async def process(self, inp: PdfChatInput, ctx: WorkflowContext[BranchResult]) -> None:
        rewritten = rewrite_question(inp.question, _to_chatml(inp.chat_history))
        await ctx.send_message(BranchResult(
            rewritten_question=rewritten,
            chat_history=inp.chat_history,
        ))


class ContextAndQnAExecutor(Executor):
    """Fan-in: finds relevant context from index, then generates answer."""

    @handler
    async def process(self, results: list[BranchResult], ctx: WorkflowContext[Never, dict]) -> None:
        index_path = None
        rewritten_question = None
        chat_history: list = []
        for r in results:
            if r.index_path:
                index_path = r.index_path
            if r.rewritten_question:
                rewritten_question = r.rewritten_question
            if r.chat_history:
                chat_history = r.chat_history

        prompt, context = find_context(rewritten_question, index_path)

        stream = qna(prompt, _to_chatml(chat_history))
        answer = "".join(stream)

        await ctx.yield_output({
            "answer": answer,
            "context": [c.text for c in context],
        })


# ---------------------------------------------------------------------------
# Workflow: input → fan_out[index, rewrite] → fan_in → context_qna
# ---------------------------------------------------------------------------

def create_workflow():
    """Create a fresh workflow instance.

    MAF workflows do not support concurrent execution, so each
    concurrent caller needs its own workflow instance.
    """
    _input = InputExecutor(id="input")
    _index = IndexExecutor(id="index")
    _rewrite = RewriteExecutor(id="rewrite")
    _context_qna = ContextAndQnAExecutor(id="context_qna")
    return (
        WorkflowBuilder(name="ChatWithPdfWorkflow", start_executor=_input)
        .add_fan_out_edges(_input, [_index, _rewrite])
        .add_fan_in_edges([_index, _rewrite], _context_qna)
        .build()
    )


async def main():
    workflow = create_workflow()
    result = await workflow.run(
        PdfChatInput(
            question="What is BERT?",
            pdf_url="https://arxiv.org/pdf/1810.04805.pdf",
        )
    )
    output = result.get_outputs()[0]
    print(f"Answer: {output['answer']}")
    print(f"Context: {output['context']}")


if __name__ == "__main__":
    asyncio.run(main())
