# -*- coding: utf-8 -*-
"""
Core Computation Functions for Graders

This module contains core computation functions used by various graders.
All computation logic is centralized here to avoid code duplication.
"""

from collections import Counter
from typing import List, Optional, Tuple

import numpy as np


def compute_bleu_score(
    reference: str,
    response: str,
    max_ngram_order: int = 4,
    smooth_method: str = "exp",
    effective_order: bool = True,
) -> Tuple[float, dict]:
    """
    Compute BLEU score using sacrebleu

    Returns:
        Tuple[float, dict]: (normalized_score [0, 1], details)
    """
    try:
        from sacrebleu.metrics import BLEU
    except ImportError:
        return 0.0, {
            "error": "sacrebleu not installed. Please install: pip install sacrebleu",
        }

    bleu = BLEU(
        max_ngram_order=max_ngram_order,
        smooth_method=smooth_method,
        effective_order=effective_order,
    )
    refs = [[reference]]

    try:
        result = bleu.corpus_score([response], refs)
        normalized_score = result.score / 100.0
        normalized_score = max(0.0, min(1.0, normalized_score))

        details = {
            "precisions": [p / 100.0 for p in result.precisions],
            "bp": result.bp,
            "sys_len": result.sys_len,
            "ref_len": result.ref_len,
            "ratio": result.sys_len / result.ref_len if result.ref_len > 0 else 0,
            "raw_score": result.score,
        }
        return normalized_score, details
    except Exception as e:
        return 0.0, {"error": str(e)}


def compute_sentence_bleu(
    reference: str,
    response: str,
    weights: tuple = (0.25, 0.25, 0.25, 0.25),
    smoothing_function: int = 1,
) -> Tuple[float, dict]:
    """
    Compute sentence-level BLEU score using NLTK

    Returns:
        Tuple[float, dict]: (score, details)
    """
    try:
        from nltk.translate.bleu_score import SmoothingFunction, sentence_bleu
    except ImportError:
        return 0.0, {"error": "NLTK not installed. Please install: pip install nltk"}

    response_tokens = response.split()
    reference_tokens = [reference.split()]

    smoothing = SmoothingFunction()
    smooth_func = getattr(smoothing, f"method{smoothing_function}")

    try:
        score = sentence_bleu(
            reference_tokens,
            response_tokens,
            weights=weights,
            smoothing_function=smooth_func,
        )
        details = {
            "weights": weights,
            "smoothing_method": smoothing_function,
            "num_references": len(reference_tokens),
        }
        return score, details
    except Exception as e:
        return 0.0, {"error": str(e)}


def compute_gleu_score(
    reference: str,
    response: str,
    min_len: int = 1,
    max_len: int = 4,
) -> Tuple[float, dict]:
    """
    Compute GLEU score using NLTK

    Returns:
        Tuple[float, dict]: (score, details)
    """
    try:
        from nltk.translate.gleu_score import sentence_gleu
    except ImportError:
        return 0.0, {
            "error": "NLTK not installed",
            "message": "Please install: pip install nltk",
        }

    response_tokens = response.split()
    reference_tokens = [reference.split()]

    try:
        score = sentence_gleu(
            reference_tokens,
            response_tokens,
            min_len=min_len,
            max_len=max_len,
        )
        details = {
            "min_len": min_len,
            "max_len": max_len,
            "num_references": len(reference_tokens),
            "response_length": len(response_tokens),
        }
        return score, details
    except Exception as e:
        return 0.0, {"error": str(e)}


def compute_chrf_score(
    reference: str,
    response: str,
    char_order: int = 6,
    beta: float = 2.0,
) -> Tuple[float, dict]:
    """
    Compute ChrF score using sacrebleu

    Returns:
        Tuple[float, dict]: (normalized_score [0, 1], details)
    """
    try:
        from sacrebleu.metrics import CHRF
    except ImportError:
        return 0.0, {
            "error": "sacrebleu not installed",
            "message": "Please install: pip install sacrebleu",
        }

    refs = [[reference]]

    try:
        chrf = CHRF(char_order=char_order, beta=beta)
        result = chrf.corpus_score([response], refs)
        normalized_score = result.score / 100.0

        details = {
            "char_order": char_order,
            "beta": beta,
            "raw_score": result.score,
        }
        return normalized_score, details
    except Exception as e:
        return 0.0, {"error": str(e)}


def compute_meteor_score(
    reference: str,
    response: str,
    alpha: float = 0.9,
    beta: float = 3.0,
    gamma: float = 0.5,
) -> Tuple[float, dict]:
    """
    Compute METEOR score using NLTK

    Returns:
        Tuple[float, dict]: (score, details)
    """
    try:
        from nltk.translate.meteor_score import meteor_score
    except ImportError:
        return 0.0, {
            "error": "NLTK not installed or missing dependencies",
            "message": "Please install: pip install nltk",
        }

    response_tokens = response.split()
    reference_tokens = reference.split()

    try:
        score = meteor_score(
            [reference_tokens],
            response_tokens,
            alpha=alpha,
            beta=beta,
            gamma=gamma,
        )
        details = {
            "alpha": alpha,
            "beta": beta,
            "gamma": gamma,
        }
        return score, details
    except Exception as e:
        return 0.0, {"error": str(e)}


def compute_rouge_scores(
    reference: str,
    response: str,
    rouge_types: List[str],
    use_stemmer: bool = True,
    score_key: str = "fmeasure",
) -> Tuple[float, dict]:
    """
    Compute ROUGE scores using rouge_score library

    Returns:
        Tuple[float, dict]: (average_score, details)
    """
    try:
        from rouge_score import rouge_scorer
    except ImportError:
        return 0.0, {
            "error": "rouge_score not installed",
            "message": "Please install: pip install rouge-score",
        }

    scorer = rouge_scorer.RougeScorer(rouge_types, use_stemmer=use_stemmer)
    scores = scorer.score(reference, response)

    aggregated = {}
    for rouge_type in rouge_types:
        score_obj = scores[rouge_type]
        if score_key == "precision":
            aggregated[rouge_type] = score_obj.precision
        elif score_key == "recall":
            aggregated[rouge_type] = score_obj.recall
        else:  # fmeasure
            aggregated[rouge_type] = score_obj.fmeasure

    avg_score = sum(aggregated.values()) / len(aggregated)

    details = {
        **aggregated,
        "rouge_types": rouge_types,
        "use_stemmer": use_stemmer,
        "score_key": score_key,
    }

    return avg_score, details


def compute_rouge_ngram(
    reference: str,
    response: str,
    n: int,
    score_type: str = "fmeasure",
) -> Tuple[float, dict]:
    """
    Compute ROUGE N-gram score (custom implementation)

    Returns:
        Tuple[float, dict]: (score, details)
    """

    def _get_ngrams(text: str, n: int) -> List[tuple]:
        tokens = text.split()
        ngrams = []
        for i in range(len(tokens) - n + 1):
            ngrams.append(tuple(tokens[i : i + n]))
        return ngrams

    ref_ngrams = _get_ngrams(reference, n)
    cand_ngrams = _get_ngrams(response, n)

    ref_counter = Counter(ref_ngrams)
    cand_counter = Counter(cand_ngrams)

    overlap = cand_counter & ref_counter
    overlap_count = sum(overlap.values())

    ref_count = sum(ref_counter.values())
    cand_count = sum(cand_counter.values())

    if cand_count == 0:
        precision = 0.0
    else:
        precision = overlap_count / cand_count

    if ref_count == 0:
        recall = 0.0
    else:
        recall = overlap_count / ref_count

    if precision + recall == 0:
        fmeasure = 0.0
    else:
        fmeasure = 2 * precision * recall / (precision + recall)

    if score_type == "precision":
        score = precision
    elif score_type == "recall":
        score = recall
    else:
        score = fmeasure

    details = {
        "precision": precision,
        "recall": recall,
        "fmeasure": fmeasure,
        "overlap_count": overlap_count,
        "reference_count": ref_count,
        "response_count": cand_count,
        "n": n,
        "score_type": score_type,
    }

    return score, details


def compute_f1_score(
    reference: str,
    response: str,
    normalize: bool = True,
) -> Tuple[float, dict]:
    """
    Compute token-based F1 score

    Returns:
        Tuple[float, dict]: (f1_score, details)
    """
    if normalize:
        response_norm = response.lower().strip()
        reference_norm = reference.lower().strip()
    else:
        response_norm = response
        reference_norm = reference

    response_tokens = response_norm.split()
    reference_tokens = reference_norm.split()

    if len(response_tokens) == 0 or len(reference_tokens) == 0:
        if len(response_tokens) == 0 and len(reference_tokens) == 0:
            return 1.0, {"precision": 1.0, "recall": 1.0}
        else:
            return 0.0, {"precision": 0.0, "recall": 0.0}

    response_counter = Counter(response_tokens)
    reference_counter = Counter(reference_tokens)
    common = response_counter & reference_counter
    num_same = sum(common.values())

    if num_same == 0:
        return 0.0, {"precision": 0.0, "recall": 0.0}

    precision = 1.0 * num_same / len(response_tokens)
    recall = 1.0 * num_same / len(reference_tokens)
    f1 = (2 * precision * recall) / (precision + recall)

    details = {
        "precision": precision,
        "recall": recall,
        "f1_score": f1,
        "common_tokens": num_same,
    }

    return f1, details


def compute_fuzzy_match(
    reference: str,
    response: str,
    method: str = "ratio",
    threshold: float = 0.8,
) -> Tuple[float, dict]:
    """
    Compute fuzzy match score using Levenshtein distance

    Returns:
        Tuple[float, dict]: (score, details)
    """
    try:
        import Levenshtein
    except ImportError:
        return 0.0, {
            "error": "python-Levenshtein not installed. Please install: pip install python-Levenshtein",
        }

    if method == "ratio":
        score = Levenshtein.ratio(response, reference)
    elif method == "partial_ratio":
        score = _partial_ratio(response, reference)
    elif method == "token_sort_ratio":
        score = _token_sort_ratio(response, reference)
    else:
        raise ValueError(
            f"Unknown method: {method}. Use 'ratio', 'partial_ratio', or 'token_sort_ratio'",
        )

    matched = score >= threshold

    details = {
        "method": method,
        "threshold": threshold,
        "matched": matched,
    }

    return score, details


def _partial_ratio(s1: str, s2: str) -> float:
    """Partial string matching"""
    import Levenshtein

    if len(s1) == 0 or len(s2) == 0:
        return 0.0 if s1 != s2 else 1.0

    shorter, longer = (s1, s2) if len(s1) <= len(s2) else (s2, s1)
    m = len(shorter)
    max_ratio = 0.0

    for i in range(len(longer) - m + 1):
        ratio = Levenshtein.ratio(shorter, longer[i : i + m])
        max_ratio = max(max_ratio, ratio)

    return max_ratio


def _token_sort_ratio(s1: str, s2: str) -> float:
    """Token order-independent fuzzy matching"""
    import Levenshtein

    tokens1 = sorted(s1.split())
    tokens2 = sorted(s2.split())
    return Levenshtein.ratio(" ".join(tokens1), " ".join(tokens2))


def compute_edit_distance(
    reference: str,
    response: str,
    normalize_by_length: bool = True,
) -> Tuple[float, dict]:
    """
    Compute edit distance similarity

    Returns:
        Tuple[float, dict]: (normalized_score, details)
    """
    try:
        import Levenshtein
    except ImportError:
        return 0.0, {
            "error": "python-Levenshtein not installed. Please install: pip install python-Levenshtein",
        }

    raw_distance = Levenshtein.distance(response, reference)
    max_len = max(len(response), len(reference))

    if normalize_by_length and max_len > 0:
        normalized_score = 1.0 - (raw_distance / max_len)
    else:
        normalized_score = 1.0 / (1.0 + raw_distance)

    normalized_score = max(0.0, min(1.0, normalized_score))

    details = {
        "raw_distance": raw_distance,
        "max_length": max_len,
        "normalize_by_length": normalize_by_length,
    }

    return normalized_score, details


def compute_cosine_similarity(
    reference: str,
    response: str,
    use_tfidf: bool = True,
    ngram_range: tuple = (1, 2),
    max_features: Optional[int] = None,
) -> Tuple[float, dict]:
    """
    Compute cosine similarity

    Returns:
        Tuple[float, dict]: (similarity_score, details)
    """
    if use_tfidf:
        score = _cosine_tfidf(response, reference, ngram_range, max_features)
    else:
        score = _cosine_simple(response, reference)

    details = {
        "use_tfidf": use_tfidf,
        "ngram_range": ngram_range,
    }

    return score, details


def _cosine_similarity_vectors(vec1: np.ndarray, vec2: np.ndarray) -> float:
    """Calculate cosine similarity between two vectors"""
    norm1 = np.linalg.norm(vec1)
    norm2 = np.linalg.norm(vec2)

    if norm1 == 0 or norm2 == 0:
        return 0.0

    similarity = float(np.dot(vec1, vec2) / (norm1 * norm2))
    return max(0.0, min(similarity, 1.0))


def _cosine_tfidf(
    text1: str,
    text2: str,
    ngram_range: tuple,
    max_features: Optional[int],
) -> float:
    """TF-IDF based cosine similarity"""
    try:
        from sklearn.feature_extraction.text import TfidfVectorizer
    except ImportError:
        return 0.0

    try:
        vectorizer = TfidfVectorizer(ngram_range=ngram_range, max_features=max_features)
        vectors = vectorizer.fit_transform([text1, text2])
        vec1 = vectors[0].toarray().flatten()
        vec2 = vectors[1].toarray().flatten()
    except Exception:
        return 0.0

    return _cosine_similarity_vectors(vec1, vec2)


def _cosine_simple(text1: str, text2: str) -> float:
    """Simple term frequency based cosine similarity"""
    words1 = text1.split()
    words2 = text2.split()

    counter1 = Counter(words1)
    counter2 = Counter(words2)

    all_words = set(counter1.keys()) | set(counter2.keys())
    if not all_words:
        return 0.0

    vec1 = np.array([counter1.get(word, 0) for word in all_words])
    vec2 = np.array([counter2.get(word, 0) for word in all_words])

    return _cosine_similarity_vectors(vec1, vec2)


def compute_jaccard_similarity(
    reference: str,
    response: str,
    use_ngrams: bool = False,
    n: int = 2,
) -> Tuple[float, dict]:
    """
    Compute Jaccard similarity

    Returns:
        Tuple[float, dict]: (similarity_score, details)
    """
    if use_ngrams:
        tokens1 = set(_ngram_tokenize(response, n))
        tokens2 = set(_ngram_tokenize(reference, n))
    else:
        tokens1 = set(response.split())
        tokens2 = set(reference.split())

    if len(tokens1) == 0 and len(tokens2) == 0:
        return 1.0, {"use_ngrams": use_ngrams}

    intersection = tokens1 & tokens2
    union = tokens1 | tokens2

    if len(union) == 0:
        return 0.0, {"use_ngrams": use_ngrams}

    score = len(intersection) / len(union)

    details = {
        "use_ngrams": use_ngrams,
        "n": n if use_ngrams else None,
    }

    return score, details


def _ngram_tokenize(text: str, n: int) -> list:
    """Simple n-gram tokenization"""
    words = text.split()
    ngrams = []
    for i in range(len(words) - n + 1):
        ngrams.append(tuple(words[i : i + n]))
    return ngrams


__all__ = [
    "compute_bleu_score",
    "compute_sentence_bleu",
    "compute_gleu_score",
    "compute_chrf_score",
    "compute_meteor_score",
    "compute_rouge_scores",
    "compute_rouge_ngram",
    "compute_f1_score",
    "compute_fuzzy_match",
    "compute_edit_distance",
    "compute_cosine_similarity",
    "compute_jaccard_similarity",
]
