"""Ajan döngüsü: streaming sohbet + araç çağrıları + onay akışı."""

from __future__ import annotations

import json
import platform
import time
from pathlib import Path
from typing import Any

import openai
from openai import OpenAI

from . import tools as T
from . import ui
from .config import Config

SYSTEM_PROMPT = """Sen bir terminalde çalışan kodlama ajanısın. Kullanıcının makinesinde \
gerçek dosyalar üzerinde çalışıyorsun.

Çalışma ortamı:
- Proje kökü: {workspace}
- İşletim sistemi: {os}
- Kabuk: {shell}

Kurallar:
1. Bir dosyayı DEĞİŞTİRMEDEN ÖNCE mutlaka read_file ile oku. Tahmine dayalı düzenleme yapma.
2. Projeyi tanımıyorsan önce list_dir / find_files / search_text ile keşfet.
3. Küçük değişiklikler için edit_file, yeni dosyalar için write_file kullan.
4. Bir seferde bir adım at; aracın sonucunu gördükten sonra devam et.
5. Kod yazarken projedeki mevcut stile, isimlendirmeye ve kütüphanelere uy.
6. Emin olmadığın bir şeyi uydurma; kontrol edebiliyorsan araçla kontrol et.
7. Yaptığın değişiklikten sonra mümkünse doğrula (test, syntax kontrolü, çalıştırma).
8. Yanıtların kısa ve öz olsun. Kullanıcı hangi dilde yazdıysa o dilde cevap ver.
9. Dosya içeriğini gereksiz yere tekrar ekrana basma; ne yaptığını özetle.
10. Yıkıcı komutlardan (rm -rf, format, git reset --hard vb.) kaçın; gerekiyorsa önce sor.
"""


class Aborted(Exception):
    """Kullanıcı adımı iptal etti."""


class Agent:
    def __init__(self, cfg: Config):
        self.cfg = cfg
        self.ws = T.Workspace(cfg.workspace, unsafe=getattr(cfg, "unsafe", False))
        self.client = OpenAI(
            api_key=cfg.api_key or "dummy",
            base_url=cfg.base_url,
            timeout=180.0,
            max_retries=2,
        )
        self.tool_mode = cfg.tool_mode if cfg.tool_mode in ("native", "prompt") else "native"
        self._mode_locked = cfg.tool_mode in ("native", "prompt")
        self.messages: list[dict[str, Any]] = []
        self.always_allow: set[str] = set()
        self.undo_stack: list[tuple[Path, str | None]] = []
        self.last_reply = ""
        self.total_in = 0
        self.total_out = 0
        self._reset_system()

    # ------------------------------------------------------------ mesaj yönetimi

    def _system_text(self) -> str:
        base = SYSTEM_PROMPT.format(
            workspace=self.ws.root,
            os=f"{platform.system()} {platform.release()}",
            shell=self.cfg.shell or ("cmd.exe" if platform.system() == "Windows" else "sh"),
        )
        if self.tool_mode == "prompt":
            base += "\n\n" + T.prompt_mode_docs()
        return base

    def _reset_system(self) -> None:
        self.messages = [{"role": "system", "content": self._system_text()}]

    def clear(self) -> None:
        self._reset_system()
        self.total_in = self.total_out = 0
        self.last_reply = ""

    def set_workspace(self, path: Path) -> None:
        self.cfg.workspace = path
        self.ws = T.Workspace(path, unsafe=self.ws.unsafe)
        self.messages[0] = {"role": "system", "content": self._system_text()}

    def trim(self, keep: int = 60) -> None:
        """Bağlam şişerse en eski mesajları at (system + son N mesaj kalır)."""
        if len(self.messages) <= keep + 1:
            return
        head = self.messages[0]
        tail = self.messages[-keep:]
        # Yetim tool mesajlarıyla başlamayalım.
        while tail and tail[0].get("role") == "tool":
            tail.pop(0)
        self.messages = [head] + tail

    # ------------------------------------------------------------ API çağrısı

    def _request(self, use_tools: bool, with_usage: bool = True):
        kwargs: dict[str, Any] = {
            "model": self.cfg.model,
            "messages": self.messages,
            "stream": True,
            "temperature": self.cfg.temperature,
            "max_tokens": self.cfg.max_tokens,
        }
        if with_usage:
            kwargs["stream_options"] = {"include_usage": True}
        if use_tools:
            kwargs["tools"] = T.SCHEMAS
            kwargs["tool_choice"] = "auto"
        return self.client.chat.completions.create(**kwargs)

    def _stream_once(self, renderer: ui.StreamRenderer, use_tools: bool):
        """Tek bir tamamlama akıtır. (text, tool_calls, finish_reason) döner."""
        try:
            stream = self._request(use_tools)
        except openai.BadRequestError as exc:
            msg = str(exc).lower()
            if "stream_options" in msg:
                stream = self._request(use_tools, with_usage=False)
            elif use_tools and ("tool" in msg or "function" in msg):
                raise _ToolsUnsupported(str(exc)) from None
            else:
                raise

        text_parts: list[str] = []
        calls: dict[int, dict[str, str]] = {}
        finish = None

        for chunk in stream:
            usage = getattr(chunk, "usage", None)
            if usage:
                self.total_in += getattr(usage, "prompt_tokens", 0) or 0
                self.total_out += getattr(usage, "completion_tokens", 0) or 0
            if not getattr(chunk, "choices", None):
                continue
            choice = chunk.choices[0]
            if choice.finish_reason:
                finish = choice.finish_reason
            delta = choice.delta
            if delta is None:
                continue

            reasoning = getattr(delta, "reasoning_content", None)
            if reasoning and self.cfg.show_reasoning:
                renderer.reasoning(reasoning)

            if delta.content:
                text_parts.append(delta.content)
                renderer.write(delta.content)

            for tc in (getattr(delta, "tool_calls", None) or []):
                idx = tc.index if tc.index is not None else 0
                slot = calls.setdefault(idx, {"id": "", "name": "", "args": ""})
                if tc.id:
                    slot["id"] = tc.id
                fn = getattr(tc, "function", None)
                if fn is not None:
                    if fn.name and fn.name != slot["name"]:
                        slot["name"] += fn.name
                    if fn.arguments:
                        slot["args"] += fn.arguments

        renderer.finish()
        text = "".join(text_parts)
        ordered = [calls[i] for i in sorted(calls)]
        return text, ordered, finish

    # ------------------------------------------------------------ araç yürütme

    def _decide(self, name: str, result: T.ToolResult) -> bool:
        """Onay gerekiyorsa sorar. True = uygula."""
        if name in T.READ_ONLY:
            return True
        if self.cfg.yolo or name in self.always_allow:
            return True

        title = result.meta.get("path")
        title = self.ws.rel(title) if isinstance(title, Path) else name
        if result.preview:
            ui.preview_panel(result.preview_kind, result.preview, title)

        answer = ui.approve(result.summary or name)
        if answer == "yes":
            return True
        if answer == "always":
            self.always_allow.add(name)
            return True
        if answer == "abort":
            raise Aborted
        return False

    def _execute(self, name: str, args: dict, index: int) -> str:
        ui.tool_header(name, args, index)
        try:
            result = T.call(self.ws, name, args, shell=self.cfg.shell)
        except T.ToolError as exc:
            ui.tool_result(str(exc), failed=True)
            return f"HATA: {exc}"

        if not self._decide(name, result):
            ui.tool_result("kullanıcı reddetti", failed=True)
            return ("Kullanıcı bu işlemi reddetti. Uygulama YAPILMADI. "
                    "Farklı bir yaklaşım öner veya kullanıcıya ne yapmak istediğini sor.")

        # geri alma bilgisi
        apply = result.meta.get("apply")
        if apply and apply[0] == "write":
            path = apply[1]
            self.undo_stack.append((path, result.meta.get("old") if result.meta.get("existed") else None))

        try:
            output = T.commit(self.ws, result)
        except Exception as exc:  # noqa: BLE001
            ui.tool_result(f"{type(exc).__name__}: {exc}", failed=True)
            return f"HATA: {exc}"

        ui.tool_result(result.summary or "tamam")
        return output or "(çıktı yok)"

    # ------------------------------------------------------------ ana döngü

    def run(self, user_text: str) -> None:
        self.messages.append({"role": "user", "content": user_text})
        self.trim()
        started = time.time()
        steps = 0
        renderer = ui.StreamRenderer()

        while steps < self.cfg.max_steps:
            steps += 1
            renderer.hide_tool_calls = self.tool_mode == "prompt"
            try:
                text, raw_calls, finish = self._stream_once(
                    renderer, use_tools=(self.tool_mode == "native")
                )
            except _ToolsUnsupported as exc:
                if self._mode_locked:
                    ui.error(f"Sunucu native tool-calling kabul etmedi: {exc}")
                    return
                ui.warn("Sunucu native tool-calling desteklemiyor, prompt moduna geçiliyor.")
                self.tool_mode = "prompt"
                self.messages[0] = {"role": "system", "content": self._system_text()}
                steps -= 1
                continue
            except openai.APIStatusError as exc:
                renderer.finish()
                ui.error(f"API {exc.status_code}: {_short(exc)}")
                self.messages.pop()  # başarısız turu geçmişte bırakma
                return
            except openai.APIConnectionError as exc:
                renderer.finish()
                ui.error(f"Bağlanılamadı ({self.cfg.base_url}): {exc}")
                self.messages.pop()
                return
            except KeyboardInterrupt:
                renderer.finish()
                ui.warn("İptal edildi.")
                return

            # --- araç çağrılarını topla
            if self.tool_mode == "native":
                calls = [
                    {"id": c["id"] or f"call_{i}", "name": c["name"],
                     "args": _loads(c["args"])}
                    for i, c in enumerate(raw_calls) if c["name"]
                ]
                assistant_msg: dict[str, Any] = {"role": "assistant", "content": text or None}
                if calls:
                    assistant_msg["tool_calls"] = [
                        {"id": c["id"], "type": "function",
                         "function": {"name": c["name"],
                                      "arguments": json.dumps(c["args"], ensure_ascii=False)}}
                        for c in calls
                    ]
                self.messages.append(assistant_msg)
            else:
                calls = T.parse_prompt_tool_calls(text)
                self.messages.append({"role": "assistant", "content": text})

            if not calls:
                self.last_reply = T.strip_tool_calls(text) if self.tool_mode == "prompt" else text
                ui.usage_line(self.total_in, self.total_out, steps, time.time() - started)
                return

            # --- çalıştır
            # İptal olsa bile HER tool_call'a bir yanıt eklenmeli, aksi halde
            # sonraki istekte "yetim tool_call" hatası alınır.
            aborted = False
            for i, c in enumerate(calls, 1):
                if aborted:
                    output = "Kullanıcı akışı iptal etti; bu araç hiç çalıştırılmadı."
                else:
                    try:
                        output = self._execute(c["name"], c["args"], i)
                    except Aborted:
                        aborted = True
                        output = "Kullanıcı bu işlemi iptal etti; uygulanmadı."
                    except KeyboardInterrupt:
                        aborted = True
                        output = "Kullanıcı Ctrl+C ile durdurdu; uygulanmadı."
                if self.tool_mode == "native":
                    self.messages.append({
                        "role": "tool",
                        "tool_call_id": c["id"],
                        "content": output,
                    })
                else:
                    self.messages.append({
                        "role": "user",
                        "content": f"<tool_result name=\"{c['name']}\">\n{output}\n</tool_result>",
                    })

            if aborted:
                ui.warn("İptal edildi, kontrol sende.")
                return

            self.trim()

        ui.warn(f"Adım sınırına ulaşıldı ({self.cfg.max_steps}). /clear ile sıfırlayabilirsin.")

    # ------------------------------------------------------------ yardımcılar

    def undo(self) -> str:
        if not self.undo_stack:
            return "Geri alınacak değişiklik yok."
        path, old = self.undo_stack.pop()
        if old is None:
            try:
                path.unlink()
            except OSError as exc:
                return f"Silinemedi: {exc}"
            return f"{self.ws.rel(path)} silindi (ajan oluşturmuştu)."
        try:
            path.write_text(old, encoding="utf-8", newline="")
        except OSError as exc:
            return f"Geri alınamadı: {exc}"
        return f"{self.ws.rel(path)} eski haline döndürüldü."

    def list_models(self) -> list[str]:
        return [m.id for m in self.client.models.list().data]


class _ToolsUnsupported(Exception):
    pass


def _loads(raw: str) -> dict:
    raw = (raw or "").strip()
    if not raw:
        return {}
    try:
        val = json.loads(raw)
    except json.JSONDecodeError:
        return {"__parse_error__": raw[:500]}
    return val if isinstance(val, dict) else {"value": val}


def _short(exc: Exception, limit: int = 400) -> str:
    s = str(exc)
    return s if len(s) <= limit else s[:limit] + "..."
