"""
Module: src.analysis.ollama_client
Workorder: WO02
Purpose: Local Ollama post-processing integration for transcript summaries.

Dependencies:
    - json: Parse API responses
    - urllib: HTTP requests to local Ollama API

Inputs:
    - transcript_text: Transcript to process

Outputs:
    - Post-processing summary/action-item text

Key Functions/Classes:
    - summarize_with_ollama: Runs local summary prompt
    - build_ollama_prompt: Builds meeting protocol prompt

Tests: tests/unit/test_placeholder.py
"""
from __future__ import annotations

import json
import re
from typing import Any
from urllib import error, request

from src.shared.errors import ExternalServiceError


def build_ollama_prompt(transcript_text: str, structured_json: bool = True) -> str:
    """Build a German protocol prompt.

    Args:
        transcript_text: Transcript content.

    Returns:
        Prompt string for Ollama generation.
    """
    if not structured_json:
        return (
            "Analysiere das folgende Transkript und antworte auf Deutsch exakt in "
            "diesem Schema (kein JSON, keine zusätzlichen Überschriften):\n"
            "TITLE: <kurzer Titel>\n"
            "TOPIC: <Thema>\n"
            "SPEAKERS: <Name 1>, <Name 2>\n"
            "OUTCOMES:\n- <Ergebnis 1>\n"
            "ACTION_ITEMS:\n- <Action Item 1>\n"
            "SUMMARY:\n<Kurzzusammenfassung>\n\n"
            f"Transkript:\n{transcript_text.strip()}"
        )

    return (
        "Analysiere das folgende Transkript und antworte ausschließlich als JSON "
        "Objekt mit genau diesen Schlüsseln: summary (string), action_items "
        "(array[string]), title (string), topic (string), speakers (array[string]), "
        "outcomes (array[string]). Alle Inhalte auf Deutsch, keine Markdown-Ausgabe, "
        "kein Fließtext außerhalb des JSON.\n\n"
        f"Transkript:\n{transcript_text.strip()}"
    )


def check_ollama_availability(
    host: str = "http://localhost:11434",
    timeout_seconds: int = 3,
) -> bool:
    """Check whether local Ollama service responds.

    Args:
        host: Ollama host URL.
        timeout_seconds: Request timeout.

    Returns:
        True when the service is reachable, otherwise False.
    """
    url = host.rstrip("/") + "/api/tags"
    req = request.Request(url=url, method="GET")

    try:
        with request.urlopen(req, timeout=timeout_seconds):  # nosec B310
            return True
    except (error.URLError, TimeoutError, OSError):
        return False


def list_ollama_models(
    host: str = "http://localhost:11434",
    timeout_seconds: int = 5,
) -> list[str]:
    """List locally available Ollama model names.

    Args:
        host: Ollama host URL.
        timeout_seconds: Request timeout.

    Returns:
        List of available model names.

    Raises:
        ExternalServiceError: If Ollama is unreachable or responds invalidly.
    """
    url = host.rstrip("/") + "/api/tags"
    req = request.Request(url=url, method="GET")

    try:
        with request.urlopen(req, timeout=timeout_seconds) as response:  # nosec B310
            response_text = response.read().decode("utf-8")
    except (error.URLError, TimeoutError, OSError) as err:
        raise ExternalServiceError(
            "Ollama service is not reachable. Ensure Ollama is running locally."
        ) from err

    try:
        parsed = json.loads(response_text)
    except json.JSONDecodeError as err:
        raise ExternalServiceError("Ollama returned invalid model list response.") from err

    models = parsed.get("models", [])
    if not isinstance(models, list):
        return []

    model_names: list[str] = []
    for entry in models:
        if not isinstance(entry, dict):
            continue
        name = entry.get("name")
        if isinstance(name, str) and name.strip():
            model_names.append(name.strip())
    return model_names


def resolve_ollama_model(
    requested_model: str,
    host: str = "http://localhost:11434",
    timeout_seconds: int = 5,
) -> str:
    """Resolve a usable Ollama model name.

    Args:
        requested_model: Requested model name or `auto`.
        host: Ollama host URL.
        timeout_seconds: Request timeout.

    Returns:
        Resolved model name.

    Raises:
        ExternalServiceError: If no usable model is available.
    """
    normalized_requested = requested_model.strip().lower()
    if normalized_requested != "auto":
        return requested_model

    available_models = list_ollama_models(host=host, timeout_seconds=timeout_seconds)
    if not available_models:
        raise ExternalServiceError(
            "No Ollama models are installed locally. Please run `ollama pull <model>`."
        )

    return available_models[0]


def get_ollama_runtime_status(
    requested_model: str = "auto",
    host: str = "http://localhost:11434",
    timeout_seconds: int = 5,
) -> dict[str, Any]:
    """Return detailed Ollama runtime status for UI diagnostics.

    Args:
        requested_model: Requested model name or `auto`.
        host: Ollama host URL.
        timeout_seconds: Request timeout.

    Returns:
        Status dictionary with reachability, models and selected model.
    """
    if not check_ollama_availability(host=host, timeout_seconds=timeout_seconds):
        return {
            "reachable": False,
            "models": [],
            "selected_model": None,
            "message": "Ollama-Dienst nicht erreichbar.",
        }

    try:
        models = list_ollama_models(host=host, timeout_seconds=timeout_seconds)
    except ExternalServiceError as error:
        return {
            "reachable": True,
            "models": [],
            "selected_model": None,
            "message": str(error),
        }

    if not models:
        return {
            "reachable": True,
            "models": [],
            "selected_model": None,
            "message": "Ollama läuft, aber es sind keine Modelle installiert.",
        }

    selected_model = resolve_ollama_model(
        requested_model=requested_model,
        host=host,
        timeout_seconds=timeout_seconds,
    )

    if requested_model.strip().lower() != "auto" and requested_model not in models:
        message = (
            f"Ollama läuft. Angefordertes Modell '{requested_model}' nicht gefunden; "
            f"Fallback auf '{selected_model}'."
        )
    else:
        message = f"Ollama läuft. Aktives Modell: {selected_model}."

    return {
        "reachable": True,
        "models": models,
        "selected_model": selected_model,
        "message": message,
    }


def _normalize_string_list(value: Any) -> list[str]:
    if not isinstance(value, list):
        return []
    return [item.strip() for item in value if isinstance(item, str) and item.strip()]


def _normalize_label(value: str) -> str:
    normalized = value.strip().lower()
    normalized = normalized.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue")
    normalized = normalized.replace("ß", "ss")
    normalized = normalized.replace("_", " ")
    normalized = re.sub(r"\s+", " ", normalized)
    return normalized


def _parse_list_items(section_text: str, split_on_commas: bool = False) -> list[str]:
    if not section_text.strip():
        return []

    lines = [line.strip() for line in section_text.splitlines() if line.strip()]
    cleaned: list[str] = []
    for line in lines:
        item = re.sub(r"^[\-*•]\s*", "", line).strip()
        if item:
            cleaned.append(item)

    if split_on_commas and len(cleaned) == 1:
        single = cleaned[0]
        comma_split = [part.strip() for part in single.split(",") if part.strip()]
        if len(comma_split) > 1:
            return comma_split

    return cleaned


def _extract_structured_sections(raw_text: str) -> dict[str, Any]:
    section_aliases = {
        "title": {"title", "titel"},
        "topic": {"topic", "thema"},
        "speakers": {"speakers", "sprecher", "teilnehmer", "participants"},
        "outcomes": {"outcomes", "ergebnisse", "entscheidungen", "results"},
        "action_items": {
            "action items",
            "action item",
            "actionitems",
            "actionitems",
            "action items (stichpunkte)",
            "naechste schritte",
            "nächste schritte",
            "todo",
            "todos",
            "aufgaben",
            "massnahmen",
            "maßnahmen",
        },
        "summary": {
            "summary",
            "zusammenfassung",
            "kurzzusammenfassung",
            "kurz zusammenfassung",
            "kurz-zusammenfassung",
        },
    }

    label_to_field: dict[str, str] = {}
    for field, labels in section_aliases.items():
        for label in labels:
            label_to_field[_normalize_label(label)] = field

    sections: dict[str, list[str]] = {key: [] for key in section_aliases}
    current_field: str | None = None

    for raw_line in raw_text.splitlines():
        stripped = raw_line.strip()
        if not stripped:
            continue

        line_without_leading_marks = re.sub(r"^[#*\-\s]+", "", stripped).strip()
        detected_field: str | None = None
        trailing_content = ""

        if ":" in line_without_leading_marks:
            label_part, value_part = line_without_leading_marks.split(":", 1)
            normalized_label = _normalize_label(label_part)
            detected_field = label_to_field.get(normalized_label)
            trailing_content = value_part.strip()

        if detected_field is not None:
            current_field = detected_field
            if trailing_content:
                sections[current_field].append(trailing_content)
            continue

        if current_field is not None:
            sections[current_field].append(stripped)

    summary_text = "\n".join(sections["summary"]).strip()
    if not summary_text:
        summary_text = raw_text.strip()

    return {
        "summary": summary_text,
        "action_items": _parse_list_items("\n".join(sections["action_items"])),
        "title": " ".join(sections["title"]).strip(),
        "topic": " ".join(sections["topic"]).strip(),
        "speakers": _parse_list_items("\n".join(sections["speakers"]), split_on_commas=True),
        "outcomes": _parse_list_items("\n".join(sections["outcomes"])),
    }


def _extract_json_object(raw_text: str) -> dict[str, Any]:
    """Extract JSON object from model text response.

    Args:
        raw_text: Raw `response` text from Ollama.

    Returns:
        Parsed JSON object.

    Raises:
        ExternalServiceError: If JSON object cannot be extracted.
    """
    candidate = raw_text.strip()
    try:
        parsed = json.loads(candidate)
        if isinstance(parsed, dict):
            return parsed
    except json.JSONDecodeError:
        pass

    start = candidate.find("{")
    end = candidate.rfind("}")
    if start == -1 or end == -1 or end <= start:
        raise ExternalServiceError("Ollama response did not contain a valid JSON object.")

    try:
        parsed_fragment = json.loads(candidate[start : end + 1])
    except json.JSONDecodeError as err:
        raise ExternalServiceError("Ollama response JSON could not be parsed.") from err

    if not isinstance(parsed_fragment, dict):
        raise ExternalServiceError("Ollama response JSON must be an object.")
    return parsed_fragment


def _build_structured_result(parsed_object: dict[str, Any]) -> dict[str, Any]:
    summary = parsed_object.get("summary")
    if not isinstance(summary, str) or not summary.strip():
        raise ExternalServiceError("Ollama JSON response missing non-empty 'summary'.")

    return {
        "summary": summary.strip(),
        "action_items": _normalize_string_list(parsed_object.get("action_items")),
        "title": str(parsed_object.get("title", "")).strip(),
        "topic": str(parsed_object.get("topic", "")).strip(),
        "speakers": _normalize_string_list(parsed_object.get("speakers")),
        "outcomes": _normalize_string_list(parsed_object.get("outcomes")),
    }


def _build_structured_result_from_text(raw_text: str) -> dict[str, Any]:
    candidate = raw_text.strip()
    if not candidate:
        raise ExternalServiceError("Ollama response did not contain generated text.")

    try:
        parsed_object = _extract_json_object(candidate)
        return _build_structured_result(parsed_object)
    except ExternalServiceError:
        return _extract_structured_sections(candidate)


def format_ollama_protocol_text(structured_output: dict[str, Any]) -> str:
    """Format structured Ollama payload as readable protocol text.

    Args:
        structured_output: Structured payload from `summarize_with_ollama`.

    Returns:
        Human-readable protocol text.
    """
    summary = str(structured_output.get("summary", "")).strip()
    action_items = structured_output.get("action_items")
    normalized_actions = (
        action_items if isinstance(action_items, list) else []
    )

    lines = ["Zusammenfassung", "", summary or "(keine Zusammenfassung)", "", "Action Items", ""]
    if normalized_actions:
        lines.extend([f"- {item}" for item in normalized_actions if isinstance(item, str) and item.strip()])
    else:
        lines.append("- (keine Action Items)")
    return "\n".join(lines).strip()


def summarize_with_ollama(
    transcript_text: str,
    model: str = "llama3.1",
    host: str = "http://localhost:11434",
    timeout_seconds: int = 60,
) -> dict[str, Any]:
    """Summarize transcript by calling local Ollama API.

    Args:
        transcript_text: Transcript content.
        model: Ollama model name.
        host: Ollama host URL.
        timeout_seconds: Request timeout.

    Returns:
        Structured protocol payload.

    Raises:
        ExternalServiceError: If Ollama is unreachable or returns invalid response.
    """
    resolved_model = resolve_ollama_model(
        requested_model=model,
        host=host,
        timeout_seconds=min(timeout_seconds, 10),
    )
    use_structured_json = not resolved_model.lower().startswith("gpt-oss")
    prompt = build_ollama_prompt(transcript_text, structured_json=use_structured_json)

    payload = {
        "model": resolved_model,
        "prompt": prompt,
        "stream": False,
    }
    if use_structured_json:
        payload["format"] = "json"

    url = host.rstrip("/") + "/api/generate"
    last_empty_details = ""

    for _ in range(2):
        data = json.dumps(payload).encode("utf-8")
        req = request.Request(
            url=url,
            data=data,
            method="POST",
            headers={"Content-Type": "application/json"},
        )

        try:
            with request.urlopen(req, timeout=timeout_seconds) as response:  # nosec B310
                response_text = response.read().decode("utf-8")
        except error.HTTPError as err:
            error_payload = ""
            try:
                error_payload = err.read().decode("utf-8")
            except Exception:
                error_payload = ""
            detail = error_payload.strip() or f"HTTP {err.code}"
            raise ExternalServiceError(f"Ollama request failed: {detail}") from err
        except (error.URLError, TimeoutError) as err:
            raise ExternalServiceError(
                "Ollama service is not reachable. Ensure Ollama is running locally."
            ) from err

        try:
            parsed_response = json.loads(response_text)
        except json.JSONDecodeError as err:
            raise ExternalServiceError("Ollama returned invalid JSON response.") from err

        if isinstance(parsed_response.get("error"), str) and parsed_response.get("error", "").strip():
            raise ExternalServiceError(f"Ollama request failed: {parsed_response['error']}")

        raw_result = parsed_response.get("response")
        if isinstance(raw_result, str) and raw_result.strip():
            if not use_structured_json:
                return _build_structured_result_from_text(raw_result)
            try:
                parsed_object = _extract_json_object(raw_result)
                return _build_structured_result(parsed_object)
            except ExternalServiceError:
                return _build_structured_result_from_text(raw_result)

        done_reason = str(parsed_response.get("done_reason", "")).strip()
        last_empty_details = done_reason or "empty-response"

    raise ExternalServiceError(
        "Ollama response did not contain generated text "
        f"(details: {last_empty_details})."
    )
