"""
RAGSentinel Evaluator - Core evaluation logic.

This module contains the main evaluation pipeline for RAGSentinel.
It handles configuration loading, LLM initialization, API communication,
Ragas metrics evaluation, and MLflow result logging.
"""

import os
import re
import yaml
import configparser
import requests
import pandas as pd
import mlflow
from dotenv import load_dotenv
from datasets import Dataset
from ragas import evaluate
from ragas.run_config import RunConfig
from ragas.metrics import (
    Faithfulness,
    AnswerRelevancy,
    ContextPrecision,
    AnswerCorrectness,
)
from langchain_openai import ChatOpenAI, OpenAIEmbeddings, AzureChatOpenAI, AzureOpenAIEmbeddings
from langchain_ollama import ChatOllama, OllamaEmbeddings


# =============================================================================
# Configuration Loading
# =============================================================================


def load_config(yaml_file='rag_eval_config.yaml'):
    """
    Load configuration from YAML file with values resolved from .env and config.ini.

    Returns:
        dict: Fully resolved configuration dictionary
    """
    load_dotenv('.env')

    ini = configparser.ConfigParser()
    ini.read('config.ini')

    def resolve(obj):
        if isinstance(obj, dict):
            return {k: resolve(v) for k, v in obj.items()}
        if isinstance(obj, list):
            return [resolve(i) for i in obj]
        if isinstance(obj, str):
            # Resolve ${ENV:VAR} and ${INI:section.key} placeholders
            result = re.sub(r'\$\{ENV:([^}]+)\}', lambda m: os.getenv(m.group(1), ''), obj)
            result = re.sub(r'\$\{INI:([^.]+)\.([^}]+)\}',
                           lambda m: ini.get(m.group(1), m.group(2), fallback=''), result)
            # Convert types
            if result.lower() == 'true': return True
            if result.lower() == 'false': return False
            try:
                if '.' in result: return float(result)
            except ValueError:
                pass
            return result
        return obj

    with open(yaml_file, 'r') as f:
        return resolve(yaml.safe_load(f))


def get_llm(config):
    """Initialize LLM based on config."""
    provider = config['ragas']['llm']['provider']

    if provider == "azure":
        azure_config = config['ragas']['llm']['azure']
        return AzureChatOpenAI(
            azure_endpoint=azure_config['endpoint'],
            api_key=azure_config['api_key'],
            deployment_name=azure_config['deployment_name'],
            model=azure_config['model'],
            temperature=azure_config['temperature'],
            api_version=azure_config['api_version']
        )
    elif provider == "openai":
        openai_config = config['ragas']['llm']['openai']
        return ChatOpenAI(
            model=openai_config['model'],
            temperature=openai_config['temperature'],
            api_key=openai_config['api_key']
        )
    elif provider == "ollama":
        ollama_config = config['ragas']['llm']['ollama']
        return ChatOllama(
            base_url=ollama_config['base_url'],
            model=ollama_config['model'],
            temperature=ollama_config['temperature']
        )
    else:
        raise ValueError(f"Unsupported LLM provider: {provider}")


def get_embeddings(config):
    """Initialize embeddings based on config."""
    provider = config['ragas']['embeddings']['provider']

    if provider == "azure":
        azure_config = config['ragas']['embeddings']['azure']
        return AzureOpenAIEmbeddings(
            azure_endpoint=azure_config['endpoint'],
            api_key=azure_config['api_key'],
            deployment=azure_config['deployment_name'],
            api_version=azure_config['api_version']
        )
    elif provider == "openai":
        openai_config = config['ragas']['embeddings']['openai']
        return OpenAIEmbeddings(
            model=openai_config['model'],
            api_key=openai_config['api_key']
        )
    elif provider == "ollama":
        ollama_config = config['ragas']['embeddings']['ollama']
        return OllamaEmbeddings(
            base_url=ollama_config['base_url'],
            model=ollama_config['model']
        )
    else:
        raise ValueError(f"Unsupported embeddings provider: {provider}")


def get_metrics(config):
    """Get Ragas metrics based on config."""
    metric_map = {
        "Faithfulness": Faithfulness(),
        "AnswerRelevancy": AnswerRelevancy(),
        "ContextPrecision": ContextPrecision(),
        "AnswerCorrectness": AnswerCorrectness(),
    }

    metric_names = config['ragas']['metrics']
    return [metric_map[name] for name in metric_names if name in metric_map]


def get_auth_headers_and_cookies(config):
    """Get authentication headers and cookies based on config."""
    auth_config = config['backend']['auth']
    auth_type = auth_config.get('type', 'none')

    headers = {}
    cookies = {}

    if auth_type == "cookie":
        cookies[auth_config['cookie_name']] = auth_config['cookie_value']
    elif auth_type == "bearer":
        headers['Authorization'] = f"Bearer {auth_config['bearer_token']}"
    elif auth_type == "header":
        headers[auth_config['header_name']] = auth_config['header_value']

    return headers, cookies


def extract_response_data(response, endpoint_config):
    """Extract data from API response."""
    if endpoint_config.get('stream', False):
        return "".join(chunk.decode() for chunk in response.iter_content(chunk_size=None))

    # Try to parse as JSON first
    try:
        data = response.json()
        response_key = endpoint_config.get('response_key')
        if response_key:
            return data.get(response_key)
        return data
    except:
        # If JSON parsing fails, return as plain text
        return response.text


def make_api_request(base_url, endpoint_config, query, chat_id, auth_headers, auth_cookies, verify_ssl=True):
    """Make API request to backend."""
    url = base_url + endpoint_config['path']
    method = endpoint_config.get('method', 'POST')

    headers = {**endpoint_config.get('headers', {}), **auth_headers}

    # Flexible body preparation
    body = {}
    for key, value in endpoint_config.get('body', {}).items():
        if isinstance(value, str) and ("{query}" in value or "{chat_id}" in value):
            body[key] = value.format(query=query, chat_id=chat_id)
        elif key == "chat_id":
            try:
                body[key] = int(chat_id)
            except (ValueError, TypeError):
                body[key] = chat_id
        else:
            body[key] = value

    if method.upper() == 'POST':
        resp = requests.post(
            url,
            json=body,
            headers=headers,
            cookies=auth_cookies,
            stream=endpoint_config.get('stream', False),
            verify=verify_ssl
        )
    elif method.upper() == 'GET':
        resp = requests.get(
            url,
            params=body,
            headers=headers,
            cookies=auth_cookies,
            verify=verify_ssl
        )
    else:
        raise ValueError(f"Unsupported HTTP method: {method}")

    resp.raise_for_status()
    return resp


def get_context(config, query, chat_id, auth_headers, auth_cookies):
    """Retrieve context from backend API."""
    base_url = config['backend']['base_url']
    endpoint_config = config['backend']['endpoints']['context']
    verify_ssl = config['backend'].get('verify_ssl', True)

    response = make_api_request(base_url, endpoint_config, query, chat_id, auth_headers, auth_cookies, verify_ssl)
    context = extract_response_data(response, endpoint_config)

    if isinstance(context, str):
        return [context]
    elif isinstance(context, list):
        return context
    else:
        return [str(context)]


def get_answer(config, query, chat_id, auth_headers, auth_cookies):
    """Get answer from backend API."""
    base_url = config['backend']['base_url']
    endpoint_config = config['backend']['endpoints']['answer']
    verify_ssl = config['backend'].get('verify_ssl', True)

    response = make_api_request(base_url, endpoint_config, query, chat_id, auth_headers, auth_cookies, verify_ssl)
    answer = extract_response_data(response, endpoint_config)

    return str(answer)



def run_evaluation():
    """Main evaluation function."""
    print("=" * 60)
    print("RAGSentinel - RAG Evaluation Framework")
    print("=" * 60)

    print("\n📁 Loading configuration...")
    config = load_config()

    dataset_path = config['dataset']['path']
    print(f"📊 Loading dataset from {dataset_path}...")
    dataset = pd.read_csv(dataset_path)

    auth_headers, auth_cookies = get_auth_headers_and_cookies(config)

    results = []
    print(f"\n🔗 Collecting responses from {config['backend']['base_url']}...")

    for idx, row in dataset.iterrows():
        chat_id = str(row['chat_id'])
        query = row['query']
        ground_truth = row['ground_truth']

        try:
            context = get_context(config, query, chat_id, auth_headers, auth_cookies)
            answer = get_answer(config, query, chat_id, auth_headers, auth_cookies)

            results.append({
                'question': query,
                'contexts': context,
                'answer': answer,
                'ground_truth': ground_truth
            })
            print(f"  ✓ Processed query {idx + 1}/{len(dataset)}: {query[:50]}...")
        except Exception as e:
            print(f"  ✗ Error processing query {idx + 1}: {e}")
            continue

    if not results:
        print("\n❌ No results collected. Exiting.")
        return

    eval_df = pd.DataFrame(results)
    print(f"\n✓ Collected {len(eval_df)} responses")

    print("\n🤖 Initializing LLM and embeddings...")
    llm = get_llm(config)
    embeddings = get_embeddings(config)

    metrics = get_metrics(config)
    print(f"   Metrics: {', '.join(config['ragas']['metrics'])}")

    print("\n📈 Preparing data for RAGAS evaluation...")
    ragas_data = {"question": [], "answer": [], "contexts": [], "ground_truth": []}

    for _, row in eval_df.iterrows():
        contexts = row.get("contexts", [])
        if not isinstance(contexts, list):
            contexts = [str(contexts)]
        contexts = [str(c) for c in contexts if c and str(c).strip()]
        if not contexts:
            contexts = ["No context available."]

        ragas_data["question"].append(str(row["question"]))
        ragas_data["answer"].append(str(row["answer"]))
        ragas_data["contexts"].append(contexts)
        ragas_data["ground_truth"].append(str(row["ground_truth"]))

    dataset = Dataset.from_dict(ragas_data)

    print("\n⏳ Evaluating with Ragas metrics (this may take a while)...")

    run_config = RunConfig(timeout=300, max_retries=3, max_wait=600)

    ragas_result = evaluate(
        dataset,
        metrics=metrics,
        llm=llm,
        embeddings=embeddings,
        batch_size=2,
        run_config=run_config,
        raise_exceptions=False
    )

    print("\n📊 Processing results...")
    scores_df = ragas_result.to_pandas()
    numeric_columns = scores_df.select_dtypes(include=['float64', 'float32', 'int64', 'int32']).columns
    mean_scores = scores_df[numeric_columns].mean().to_dict()

    mlflow_config = config['mlflow']
    mlflow.set_tracking_uri(mlflow_config['tracking_uri'])
    mlflow.set_experiment(mlflow_config['experiment_name'])

    print("\n📤 Logging results to MLflow...")
    run_name = mlflow_config.get('run_name', 'RAG Evaluation')
    with mlflow.start_run(run_name=run_name):
        print("\n" + "=" * 40)
        print("📊 EVALUATION RESULTS")
        print("=" * 40)
        for metric_name, value in mean_scores.items():
            mlflow.log_metric(metric_name, value)
            print(f"   {metric_name}: {value:.4f}")

        mlflow.log_param("dataset_path", dataset_path)
        mlflow.log_param("num_samples", len(eval_df))
        mlflow.log_table(data=scores_df, artifact_file="ragas_detailed_results.json")

    print("\n" + "=" * 60)
    print("✅ Evaluation complete!")
    print(f"🔗 View results at: {mlflow_config['tracking_uri']}")
    print("=" * 60)
