from __future__ import annotations

import json
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import requests

from ..task_catalog import load_tasks
from .store import append_import_record, append_rules, ensure_knowledge_store, normalize_rule_row


DEFAULT_MODEL = "gpt-4.1-mini"
DEFAULT_MAX_RULES = 12
MAX_SOURCE_CHARS = 60000


RULE_SCHEMA = {
    "type": "object",
    "additionalProperties": False,
    "properties": {
        "rules": {
            "type": "array",
            "items": {
                "type": "object",
                "additionalProperties": False,
                "properties": {
                    "id": {"type": "string"},
                    "topic": {"type": "string"},
                    "suggested_task_ids": {
                        "type": "array",
                        "items": {"type": "string"},
                    },
                    "rule_type": {"type": "string"},
                    "condition": {"type": "string"},
                    "recommendation": {"type": "string"},
                    "risk": {"type": "string"},
                    "source": {"type": "string"},
                    "confidence": {"type": "string"},
                    "text": {"type": "string"},
                },
                "required": [
                    "id",
                    "topic",
                    "suggested_task_ids",
                    "rule_type",
                    "condition",
                    "recommendation",
                    "risk",
                    "source",
                    "confidence",
                    "text",
                ],
            },
        },
        "notes": {"type": "string"},
    },
    "required": ["rules", "notes"],
}


@dataclass(frozen=True)
class ImportResult:
    source_path: Path
    source_name: str
    model: str
    rules_count: int
    rule_ids: list[str]
    notes: str
    dry_run: bool


def import_knowledge_file(
    file_path: Path,
    source_name: str,
    model: str | None = None,
    max_rules: int = DEFAULT_MAX_RULES,
    dry_run: bool = False,
) -> ImportResult:
    ensure_knowledge_store()
    if not file_path.exists():
        raise FileNotFoundError(f"Nie znaleziono pliku wiedzy: {file_path}")
    if not file_path.is_file():
        raise ValueError(f"Sciezka nie jest plikiem: {file_path}")
    if not source_name.strip():
        raise ValueError("Podaj --source, czyli czytelna nazwe zrodla wiedzy.")

    content = read_source_file(file_path)
    selected_model = model or os.environ.get("KNOWLEDGE_OPENAI_MODEL") or os.environ.get("OPENAI_MODEL") or DEFAULT_MODEL

    if dry_run:
        return ImportResult(
            source_path=file_path,
            source_name=source_name,
            model=selected_model,
            rules_count=0,
            rule_ids=[],
            notes="Tryb dry-run: plik odczytany, API nie zostalo wywolane, reguly nie zostaly zapisane.",
            dry_run=True,
        )

    api_key = os.environ.get("OPENAI_API_KEY")
    if not api_key:
        raise RuntimeError(
            "Brak OPENAI_API_KEY. Dodaj klucz do .env albo uruchom z --dry-run, "
            "zeby tylko sprawdzic plik wejsciowy."
        )

    payload = call_openai_extractor(
        api_key=api_key,
        model=selected_model,
        source_name=source_name,
        content=content,
        max_rules=max_rules,
    )
    raw_rules = payload.get("rules") or []
    normalized_rules = []
    for row in raw_rules:
        normalized = normalize_rule_row({**row, "source": source_name, "task_ids": []})
        normalized_rules.append(normalized)

    saved_rules = append_rules(normalized_rules, source_file=str(file_path))
    notes = str(payload.get("notes") or "").strip()
    append_import_record(
        {
            "file": str(file_path),
            "source": source_name,
            "model": selected_model,
            "rules_count": len(saved_rules),
            "notes": notes,
        }
    )
    return ImportResult(
        source_path=file_path,
        source_name=source_name,
        model=selected_model,
        rules_count=len(saved_rules),
        rule_ids=[rule.id for rule in saved_rules],
        notes=notes,
        dry_run=False,
    )


def read_source_file(path: Path) -> str:
    content = path.read_text(encoding="utf-8-sig", errors="replace")
    content = content.strip()
    if not content:
        raise ValueError(f"Plik wiedzy jest pusty: {path}")
    if len(content) > MAX_SOURCE_CHARS:
        return content[:MAX_SOURCE_CHARS]
    return content


def call_openai_extractor(
    api_key: str,
    model: str,
    source_name: str,
    content: str,
    max_rules: int,
) -> dict[str, Any]:
    base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1").rstrip("/")
    endpoint = f"{base_url}/responses"
    response = requests.post(
        endpoint,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        json={
            "model": model,
            "instructions": build_extractor_instructions(source_name, max_rules),
            "input": build_extractor_input(source_name, content),
            "text": {
                "format": {
                    "type": "json_schema",
                    "name": "knowledge_rules",
                    "strict": True,
                    "schema": RULE_SCHEMA,
                }
            },
            "max_output_tokens": 6000,
            "store": False,
        },
        timeout=120,
    )
    if response.status_code >= 400:
        raise RuntimeError(f"OpenAI API zwrocilo blad {response.status_code}: {response.text}")
    data = response.json()
    output_text = extract_output_text(data)
    if not output_text:
        raise RuntimeError("OpenAI API nie zwrocilo tekstu z regułami.")
    try:
        payload = json.loads(output_text)
    except json.JSONDecodeError as exc:
        raise RuntimeError(f"OpenAI API zwrocilo nieprawidlowy JSON: {exc}") from exc
    if not isinstance(payload, dict):
        raise RuntimeError("OpenAI API powinno zwrocic obiekt JSON.")
    return payload


def extract_output_text(data: dict[str, Any]) -> str:
    if isinstance(data.get("output_text"), str):
        return data["output_text"]
    parts: list[str] = []
    for item in data.get("output", []) or []:
        for content in item.get("content", []) or []:
            if content.get("type") == "output_text" and isinstance(content.get("text"), str):
                parts.append(content["text"])
            elif isinstance(content.get("text"), str):
                parts.append(content["text"])
    return "\n".join(parts).strip()


def build_extractor_instructions(source_name: str, max_rules: int) -> str:
    task_lines = []
    for task in load_tasks():
        task_lines.append(
            f"- {task.id}: {task.group_name} / {task.name}. {task.description}"
        )
    tasks_block = "\n".join(task_lines)
    return f"""
Jestes parserem wiedzy dla terminalowego narzedzia Google Ads.
Masz zamienic material zrodlowy na maksymalnie {max_rules} atomowych regul.

Zrodlo ma nazwe: {source_name}

Aktualnie istniejace task_id w narzedziu:
{tasks_block}

Wymagania:
- Pisz po polsku.
- Wszystkie pola opisowe (`condition`, `recommendation`, `risk`, `text`, `notes`) musza byc po polsku.
- Zwracaj tylko JSON zgodny ze schematem.
- Jedna regula ma opisywac jeden konkretny wniosek, warunek, ryzyko albo rekomendacje.
- Nie tworz regul ogolnikowych typu "monitoruj wyniki"; regula ma mowic co sprawdzic i dlaczego.
- suggested_task_ids wypelniaj tylko istniejacymi task_id z listy powyzej.
- suggested_task_ids ustaw tylko wtedy, gdy regula bezposrednio dotyczy danego zadania i moze byc uzyta w jego planie.
- Nie proponuj zadania tylko dlatego, ze regula dotyczy podobnego typu kampanii albo ogolnej optymalizacji.
- Jesli regula jest wazna, ale nie pasuje bezposrednio do istniejacego zadania, ustaw suggested_task_ids na [].
- Narzedzie zapisze suggested_task_ids jako propozycje. Zadanie zostanie dopisane do task_ids dopiero po akceptacji uzytkownika.
- source w kazdej regule ustaw dokladnie jako nazwe zrodla podana powyzej.
- confidence ustaw jako low, medium albo high.
- rule_type ustaw jako audit_check, recommendation, warning albo implementation_note.
- topic ustaw krotko, np. search, pmax, shopping, feed-merchant, konwersje, ga4, gtm-tracking, negative-keywords, strategie-stawek, produkty.
- id ma byc krotkim stabilnym identyfikatorem po angielsku albo bez polskich znakow.
- text ma byc samodzielnym, jednozdaniowym opisem reguly.
""".strip()


def build_extractor_input(source_name: str, content: str) -> str:
    return f"""
Przetworz ponizszy material na atomowe reguly dla narzedzia Google Ads.

Zrodlo: {source_name}

MATERIAL:
{content}
""".strip()
