"""
// Copyright (C) 2025 Aleutian AI (jinterlante@aleutian.ai)
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// See the LICENSE.txt file for the full license text.
//
// NOTE: This work is subject to additional terms under AGPL v3 Section 7.
// See the NOTICE.txt file for details regarding AI system attribution.

git clean -xdf && rm -rf build dist .eggs aleutian_client.egg-info && pip install -e .
"""

import httpx
from .models import (
    RAGRequest, RAGResponse,
    DirectChatRequest, DirectChatResponse, Message,
    DocumentRequest, DocumentResponse,
    SessionListResponse, SessionInfo, DeleteSessionResponse, WeaviateGraphQLResponse,
    TimeseriesForecastRequest, TimeseriesForecastResponse, DataFetchResponse, DataFetchRequest
)
from .exceptions import AleutianConnectionError, AleutianApiError
from typing import List, Optional


class AleutianClient:
    def __init__(self, host: str = "http://localhost", port: int = 12210):
        """
        Initializes the client to connect to an Aleutian orchestrator.

        Args:
            host: The hostname of the orchestrator (e.g., http://localhost)
            port: The port the orchestrator is listening on (default 12210)
        """
        self.base_url = f"{host}:{port}"
        self._client = httpx.Client(base_url=self.base_url, timeout=300.0)  # 5 min timeout

    def _handle_error(self, response: httpx.Response, endpoint: str):
        """Helper to parse and raise API errors."""
        if response.status_code >= 400:
            try:
                error_data = response.json()
                # Check for specific error structures
                error_detail = error_data.get('error', response.text)
                if 'details' in error_data:  # As seen in orchestrator 500 responses
                    error_detail = f"{error_detail}: {error_data.get('details')}"
            except Exception:
                error_detail = response.text
            raise AleutianApiError(
                f"API call to {endpoint} failed with status {response.status_code}: {error_detail}"
            )

    def health_check(self) -> dict:
        """Checks the health of the orchestrator."""
        try:
            response = self._client.get("/health")
            response.raise_for_status()  # Raise HTTPError for 4xx/5xx
            return response.json()
        except httpx.RequestError as e:
            raise AleutianConnectionError(f"Connection failed: {e}") from e

    def ask(self, query: str, pipeline: str = "reranking", no_rag: bool = False,
            session_id: str = None) -> RAGResponse:
        """
        Asks a question to the RAG system (maps to /rag).

        Args:
            query: The user's question.
            pipeline: The RAG pipeline to use (e.g., "reranking", "standard").
            no_rag: Set to True to skip RAG and ask the LLM directly.
            session_id: An optional session ID.

        Returns:
            A RAGResponse object with the answer and sources.
        """
        request_data = RAGRequest(query=query, pipeline=pipeline, no_rag=no_rag,
                                  session_id=session_id)
        try:
            endpoint = "/v1/rag"
            response = self._client.post(endpoint, json=request_data.model_dump())

            if response.status_code != 200:
                # Try to parse the error detail from the server
                try:
                    error_detail = response.json().get('details', response.text)
                except Exception:
                    error_detail = response.text
                raise AleutianApiError(
                    f"API returned status {response.status_code}: {error_detail}")

            return RAGResponse(**response.json())

        except httpx.RequestError as e:
            raise AleutianConnectionError(f"Connection to /rag failed: {e}") from e

    def chat(self, messages: list[Message]) -> DirectChatResponse:
        """
        Sends a list of messages to the direct chat endpoint (maps to /chat/direct).

        Args:
            messages: A list of Message objects (e.g., [Message(role="user", content="Hi")])

        Returns:
            A DirectChatResponse object with the assistant's answer.
        """
        request_data = DirectChatRequest(messages=messages)
        try:
            endpoint = "/v1/chat/direct"
            response = self._client.post(endpoint, json=request_data.model_dump())
            if response.status_code != 200:
                try:
                    error_detail = response.json().get('error', response.text)
                except Exception:
                    error_detail = response.text
                raise AleutianApiError(
                    f"API returned status {response.status_code}: {error_detail}")

            return DirectChatResponse(**response.json())

        except httpx.RequestError as e:
            raise AleutianConnectionError(f"Connection to /chat/direct failed: {e}") from e

    def populate_document(self, content: str, source: str,
                          version: Optional[str] = None) -> DocumentResponse:
        """
        Populates a single document into Weaviate (maps to POST /documents).
        Note: This sends raw content. For file-path based ingestion, use the CLI.

        Args:
            content: The text content of the document.
            source: A source identifier (e.g., file path, URL).
            version: (Optional) A version string for data tracking.

        Returns:
            A DocumentResponse object indicating success or skip.
        """
        # Note: The Go handler 'documents.go' doesn't seem to use a 'version' field yet.
        # We send it, but it might be ignored until the handler is updated.
        request_data = DocumentRequest(content=content, source=source, version=version)

        try:
            # Note: The 'documents.go' handler expects 'content' and 'source' at the top level
            # Let's match the Go struct 'CreateDocumentRequest'
            endpoint = "/v1/documents"
            response = self._client.post(endpoint,
                                         json=request_data.model_dump(exclude_none=True))
            self._handle_error(response, endpoint)
            return DocumentResponse(**response.json())
        except httpx.RequestError as e:
            raise AleutianConnectionError(f"Connection to /documents failed: {e}") from e

    def list_sessions(self) -> List[SessionInfo]:
        """
        Lists all available conversation sessions (maps to GET /sessions).

        Returns:
            A list of SessionInfo objects.
        """
        try:
            endpoint = "/v1/sessions"
            response = self._client.get(endpoint)
            self._handle_error(response, endpoint)
            parsed_response = WeaviateGraphQLResponse(**response.json())
            if parsed_response.Get and "Session" in parsed_response.Get:
                return parsed_response.Get["Session"]
            return []
        except httpx.RequestError as e:
            raise AleutianConnectionError(f"Connection to /sessions failed: {e}") from e

    def delete_session(self, session_id: str) -> DeleteSessionResponse:
        """
        Deletes a specific session and its related conversations (maps to DELETE /sessions/{session_id}).

        Args:
            session_id: The ID of the session to delete.

        Returns:
            A DeleteSessionResponse object confirming deletion.
        """
        endpoint = f"/v1/sessions/{session_id}"
        try:
            response = self._client.delete(endpoint)
            self._handle_error(response, endpoint)
            return DeleteSessionResponse(**response.json())
        except httpx.RequestError as e:
            raise AleutianConnectionError(f"Connection to {endpoint} failed: {e}") from e

    def close(self):
        """
        Safe-closes the underlying HTTP client.
        Call this when you are done with the client instance.
        """
        if self._client and not self._client.is_closed:
            self._client.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def forecast(self, name: str, context_period_size: int,
                 forecast_period_size: int) -> TimeseriesForecastResponse:
        """
        Calls the orchestrator's timeseries forecast endpoint.

        Args:
            name: The name of the thing to forecast (e.g., "SPY").
            context_period_size: The number of past days to use as context.
            forecast_period_size: The number of future days to forecast.

        Returns:
            A TimeseriesForecastResponse object with the forecast data.
        """
        request_data = TimeseriesForecastRequest(
            name=name,
            context_period_size=context_period_size,
            forecast_period_size=forecast_period_size
        )
        try:
            endpoint = "/v1/timeseries/forecast"
            response = self._client.post(endpoint, json=request_data.model_dump())
            self._handle_error(response, endpoint)
            return TimeseriesForecastResponse(**response.json())
        except httpx.RequestError as e:
            raise AleutianConnectionError(f"Connection to {endpoint} failed: {e}") from e
        except Exception as e:
            raise AleutianApiError(f"Failed to parse forecast response: {e}") from e

    def ensure_data(self, names: List[str], start_date: str,
                    interval: str = "1d") -> DataFetchResponse:
        """
        Calls the orchestrator's on-demand data fetch endpoint.
        This will trigger the finance-data-service to pull data
        from Yahoo and store it in InfluxDB.

        Args:
            names: A list of tickers for example (e.g., ["SPY", "MSFT"]).
            start_date: The earliest date to fetch (e.g., "2020-01-01").
            interval: The data frequency (e.g., "1d", "1h", "1m").

        Returns:
            A DataFetchResponse object with the status.
        """
        request_data = DataFetchRequest(
            names=names,
            start_date=start_date,
            interval=interval
        )
        try:
            endpoint = "/v1/data/fetch"
            response = self._client.post(endpoint, json=request_data.model_dump())
            self._handle_error(response, endpoint)
            return DataFetchResponse(**response.json())
        except httpx.RequestError as e:
            raise AleutianConnectionError(f"Connection to {endpoint} failed: {e}") from e
        except Exception as e:
            raise AleutianApiError(f"Failed to parse data fetch response: {e}") from e