import requests
import json
import time
import os
import re
import sys
from typing import List, Dict, Any

"""
v3_multi.py
-- Conversație multi-AI pe mai multe mașini (Ollama) cu roluri și reguli de noutate --

Noutăți față de v2.py:
- Suport pentru N mașini (>=2), fiecare cu IP, model, rol, timeout și limită de tokeni.
- Roluri: generator / critic / moderator / sintetizator (synth). Promptare specifică fiecărui rol.
- Reguli de noutate: fiecare răspuns trebuie să adauge cel puțin o idee nouă față de registrul global.
- Rezumat de idei menționate este injectat în prompt ca să minimizeze repetițiile.
- Log detaliat per rundă și per mașină, cu număr de tokeni (dacă API-ul returnează eval_count).
- Retry + exponential backoff pe timeouts/erori.
- Întreținere registru de fraze/idei (deduplicate pe fraze curate).

Necesită: servere Ollama accesibile la /api/chat pe fiecare IP:PORT.
"""

PORT_DEFAULT = "11434"
MAX_RETRY = 3
LOG_FILE = os.path.join(os.path.dirname(__file__), "conversatie.txt")
INTREBARE_IMPLICITA = "Ce idei concrete, ne-repetate, poți adăuga mai departe?"

# ---- Utilitare ----

def validare_ip(ip_str: str) -> bool:
    pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
    return re.match(pattern, ip_str) is not None

def write_to_log(message: str) -> None:
    with open(LOG_FILE, "a", encoding="utf-8") as f:
        f.write(message + "\n")

def norm_text(s: str) -> str:
    s = s.lower()
    s = re.sub(r'\s+', ' ', s).strip()
    return s

def extrage_fraze_idei(text: str) -> List[str]:
    """
    Heuristic simplu: împarte pe punct/linie nouă/bullet și returnează fraze curate,
    ignorând linii foarte scurte (<5 caractere).
    """
    chunks = re.split(r'[.\n•\-\u2022;]+', text)
    out = []
    for c in chunks:
        t = norm_text(c)
        if len(t) >= 5:
            out.append(t)
    return out

# ---- Promptare pe roluri ----

def sistem_role_prompt(subiect: str, rol: str, max_tokens: int) -> str:
    baza = (
        f"Discuți despre: {subiect}. Răspunde concis (max {max_tokens} tokeni), clar, în română.\n"
        "Reguli generale:\n"
        "1) Evită repetițiile. 2) Adaugă cel puțin O IDEE NOUĂ față de lista 'Idei deja menționate'. "
        "3) Listează ideile ca bullets scurte, concrete, testabile. 4) Fii specific (cifre, condiții, costuri, pași)."
    )
    if rol == "generator":
        return baza + "\nRol: GENERATOR – propune idei noi, combinații neobișnuite, dar plauzibile."
    if rol == "critic":
        return baza + "\nRol: CRITIC – semnalează riscuri/limitări și oferă o îmbunătățire alternativă pentru fiecare punct."
    if rol == "moderator":
        return baza + "\nRol: MODERATOR – impune noutatea, rezumă scurt progresul, pune o întrebare-țintă pentru runda următoare."
    if rol in ("synth", "sintetizator", "synthesizer"):
        return baza + "\nRol: SINTETIZATOR – combină cele mai bune idei, elimină redundanța, propune o mini-foaie de parcurs."
    # fallback
    return baza + "\nRol: GENERAL – adaugă idei utile și noi."

def construieste_mesaje(conversatie: List[Dict[str, str]], subiect: str, rol: str, max_tokens: int, idei_deja: List[str]) -> List[Dict[str, str]]:
    sistem = sistem_role_prompt(subiect, rol, max_tokens)
    idei_block = "Idei deja menționate până acum:\n- " + "\n- ".join(idei_deja[-30:]) if idei_deja else "Nu există idei înregistrate încă."
    instructiune = (
        f"{idei_block}\n"
        "Asigură-te că fiecare punct nou este ne-repetat față de listă. "
        f"După idei, încheie cu: 'NEXT: {INTREBARE_IMPLICITA}'"
    )
    # Pregătim istoricul conversației + instructiunea de rol
    messages = [{"role": "system", "content": sistem}]
    messages.extend(conversatie)
    messages.append({"role": "user", "content": instructiune})
    return messages

# ---- Rețea/Request ----

def api_chat_stream(url: str, payload: Dict[str, Any], timeout: int) -> Dict[str, Any]:
    retry = 0
    current_timeout = timeout
    raspuns_complet = ""
    tokens_eval = None
    while retry < MAX_RETRY:
        try:
            print(f"Încerc să trimit cerere la: http://{url}/api/chat cu modelul {payload.get('model')} (timeout: {current_timeout}s)")
            write_to_log(f"Încerc să trimit cerere la: http://{url}/api/chat cu modelul {payload.get('model')} (timeout: {current_timeout}s)")
            resp = requests.post(f"http://{url}/api/chat", json=payload, timeout=current_timeout, stream=True)
            resp.raise_for_status()
            for line in resp.iter_lines():
                if not line:
                    continue
                chunk = json.loads(line)
                if "message" in chunk and "content" in chunk["message"]:
                    token = chunk["message"]["content"]
                    raspuns_complet += token
                    sys.stdout.write(token)
                    sys.stdout.flush()
                if chunk.get("done", False):
                    tokens_eval = chunk.get("eval_count", None)
                    print()
                    break
            return {"text": raspuns_complet, "tokens": tokens_eval}
        except requests.exceptions.ReadTimeout:
            msg = f"Timeout depășit ({current_timeout}s) către {url}. Reîncerc..."
            print(msg); write_to_log(msg)
            retry += 1; current_timeout *= 2
        except requests.exceptions.RequestException as e:
            msg = f"Eroare către {url}: {e}"
            print(msg); write_to_log(msg)
            retry += 1; current_timeout *= 2
    msg = f"Maxim de încercări atins ({MAX_RETRY}) către {url}. Abort."
    print(msg); write_to_log(msg)
    return {"text": None, "tokens": None}

# ---- Main ----

def main():
    # Inițializare log
    with open(LOG_FILE, "w", encoding="utf-8") as f:
        f.write("== Conversație multi-AI ==\n")

    subiect = input("Introdu tema de discuție: ").strip()
    try:
        numar_runde = int(input("Introdu numărul de runde (0 = infinit): "))
    except ValueError:
        print("Valoare invalidă. Implicit 5 runde."); numar_runde = 5

    try:
        pauza_input = input("Pauză între runde în secunde (default 3): ").strip()
        PAUZA_INTRE_RUNDE_SEC = int(pauza_input) if pauza_input else 3
    except ValueError:
        print("Valoare invalidă. Implicit 3 secunde."); PAUZA_INTRE_RUNDE_SEC = 3

    try:
        numar_masini = int(input("Câte mașini (>=2) dorești să configurezi?: "))
    except ValueError:
        print("Valoare invalidă. Implicit 2 mașini."); numar_masini = 2
    if numar_masini < 2:
        numar_masini = 2

    masini: List[Dict[str, Any]] = []
    for idx in range(numar_masini):
        print(f"\n-- Config mașină #{idx+1} --")
        if idx == 0:
            # Default-uri pentru prima mașină
            ip_default = "100.76.154.75"
            model_default = "qwen3-coder:480b-cloud"
            max_tokens_default = 700
            timeout_default = 120
        elif idx == 1:
            # Default-uri pentru a doua mașină
            ip_default = "100.73.213.29"
            model_default = "gpt-oss:120b-cloud"
            max_tokens_default = 700
            timeout_default = 120
        else:
            # Pentru mașini suplimentare, folosește default-urile originale
            ip_default = None
            model_default = "llama3:latest"
            max_tokens_default = 200
            timeout_default = 120

        while True:
            ip_prompt = f"IP (default {ip_default}): " if ip_default else "IP (ex: 100.76.154.75): "
            ip_raw = input(ip_prompt).strip()
            if not ip_raw and ip_default:
                ip_raw = ip_default
            if validare_ip(ip_raw):
                break
            print("IP invalid, reîncearcă.")
        
        port = input(f"Port (gol pentru default {PORT_DEFAULT}): ").strip() or PORT_DEFAULT
        model_input = input(f"Model (default {model_default}): ").strip()
        model = model_default if not model_input else model_input
        rol = input("Rol [generator/critic/moderator/synth] (implicit: generator): ").strip().lower() or "generator"
        try:
            max_tokens_input = input(f"Limita tokeni răspuns (default {max_tokens_default}): ") or str(max_tokens_default)
            max_tokens = int(max_tokens_input)
        except ValueError:
            max_tokens = max_tokens_default
        try:
            timeout_input = input(f"Timeout inițial sec (default {timeout_default}): ") or str(timeout_default)
            timeout = int(timeout_input)
        except ValueError:
            timeout = timeout_default

        masini.append({
            "url": f"{ip_raw}:{port}",
            "model": model,
            "rol": rol,
            "max_tokens": max_tokens,
            "timeout": timeout
        })

    # Istoric conversație comun
    conversatie: List[Dict[str, str]] = []
    # Registru idei unice (fraze normalizate)
    registry: Dict[str, bool] = {}

    runda = 0
    while True:
        runda += 1
        if numar_runde != 0 and runda > numar_runde:
            done = f"Procesul s-a încheiat după {numar_runde} runde."
            print(done); write_to_log(done)
            break
        print(f"\n===== RUNDA {runda} ====="); write_to_log(f"\n===== RUNDA {runda} =====")

        # parcurgere round-robin
        for m in masini:
            rol = m["rol"]
            max_tok = m["max_tokens"]
            timeout = m["timeout"]
            url = m["url"]
            model = m["model"]

            idei_list = list(registry.keys())
            mesaje = construieste_mesaje(conversatie, subiect, rol, max_tok, idei_list)
            payload = {
                "model": model,
                "messages": mesaje,
                "stream": True,
                "options": {"num_predict": max_tok}
            }

            rezultat = api_chat_stream(url, payload, timeout)
            text = rezultat["text"]
            tokens = rezultat["tokens"]

            if text is None:
                stop = f"Nu s-a primit răspuns de la {url}. Oprire."
                print(stop); write_to_log(stop)
                return

            write_to_log(f"[{rol.upper()} @ {url} :: tokens={tokens}] {text}")

            # actualizează registry de idei
            fraze = extrage_fraze_idei(text)
            adaugate = 0
            for fr in fraze:
                if fr not in registry:
                    registry[fr] = True
                    adaugate += 1

            # adaugă în conversație ca mesaj al 'assistant'-ului și întrebare implicită
            conversatie.append({"role": "assistant", "content": text})
            conversatie.append({"role": "user", "content": INTREBARE_IMPLICITA})

            print(f"\n[INFO] Idei noi adăugate în registru: {adaugate}")
            write_to_log(f"[INFO] Idei noi adăugate: {adaugate}")

        time.sleep(PAUZA_INTRE_RUNDE_SEC)

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nOprire prin Ctrl+C.")
