import ast
import json
import math
import re
from typing import Any, Union

from inspect_ai.scorer._classification import compute_f1


def extract_number(s: Union[str, int, float]) -> float:
    """
    Extract a number from a string, handling various currency symbols, units, and number formats.

    :param s: Input string, integer, or float
    :return: Extracted number as a float
    """
    if isinstance(s, int | float):
        return float(s)

    # Remove thousand separators and convert decimal separators
    s = s.replace(",", "").replace(" ", "")

    # Try to find a number pattern
    match = re.search(r"-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?", s)

    if match:
        # Extract the number and convert to float
        return float(match.group())

    raise ValueError(f"Could not extract number from '{s}'")


def parse_numeric_values(data: Any) -> Any:
    """Recursively parse numeric values in dictionaries and lists, handling currency and units."""
    if isinstance(data, dict):
        return {k: parse_numeric_values(v) for k, v in data.items()}
    elif isinstance(data, list):
        return [parse_numeric_values(item) for item in data]
    elif isinstance(data, str):
        try:
            return extract_number(data)
        except ValueError:
            return data
    return data


def parse_and_classify(input_data: str) -> Any:
    """Parse the input string and classify its type, parsing numeric values where possible."""
    # Strip whitespace
    input_data = input_data.strip()

    # Try to parse as JSON first (handles dicts and lists)
    try:
        parsed_data = json.loads(input_data)
        return parse_numeric_values(parsed_data)
    except json.JSONDecodeError:
        pass

    # Try to evaluate as a Python literal (handles tuples and other structures)
    try:
        parsed_data = ast.literal_eval(input_data)
        return parse_numeric_values(parsed_data)
    except (ValueError, SyntaxError):
        pass

    # If it's a multiline string, try to parse each line as JSON
    if "\n" in input_data:
        try:
            parsed_data = [json.loads(line.strip()) for line in input_data.splitlines()]
            return parse_numeric_values(parsed_data)
        except json.JSONDecodeError:
            # If JSON parsing fails, split into lines but keep as a list of strings
            return [line.strip() for line in input_data.splitlines()]

    # Check if it's a number (including currency and units)
    try:
        return extract_number(input_data)
    except ValueError:
        pass

    # If all else fails, return as a string
    return input_data


def calculate_number_score(pred: float, gold: float) -> float:
    """Calculate score for numeric predictions."""
    if pred == gold:
        return 1.0

    # If both are negative, just make them positive
    if pred < 0 and gold < 0:
        pred, gold = abs(pred), abs(gold)
    # If one is negative, add the absolute difference to both
    elif pred < 0 or gold < 0:
        diff = abs(abs(pred) - abs(gold))
        pred += diff
        gold += diff

    # Calculate the ratio
    ratio = max(pred, gold) / min(pred, gold) if min(pred, gold) > 0 else float("inf")

    # Apply the order magnitude metric
    score = max(0, 1 - math.log10(ratio))

    return score


def calculate_json_score(pred: dict[str, Any], gold: dict[str, Any]) -> float:
    """Calculate score for JSON (dictionary) predictions, handling numeric comparisons."""
    if len(pred) == 0 and len(gold) == 0:
        return 1

    all_keys = set(pred.keys()) | set(gold.keys())
    scores = []

    for key in all_keys:
        if key not in pred or key not in gold:
            scores.append(0.0)
        elif isinstance(gold[key], int | float) and isinstance(pred[key], int | float):
            scores.append(calculate_number_score(float(pred[key]), float(gold[key])))
        elif isinstance(gold[key], str) and isinstance(pred[key], str):
            scores.append(compute_f1(pred[key], gold[key]))
        elif isinstance(gold[key], dict) and isinstance(pred[key], dict):
            scores.append(calculate_json_score(pred[key], gold[key]))
        elif isinstance(gold[key], list) and isinstance(pred[key], list):
            scores.append(score_answer(pred[key], gold[key]))
        else:
            scores.append(0.0)

    return sum(scores) / len(scores) if scores else 0.0


def score_answer(
    pred: Union[str, int, float, list[Any], dict[str, Any]],
    gold: Union[str, int, float, list[Any], dict[str, Any]],
) -> float:
    """Score the answer provided by the model against the gold standard."""
    pred = parse_and_classify(pred) if isinstance(pred, str) else pred
    gold = parse_and_classify(gold) if isinstance(gold, str) else gold

    if isinstance(gold, int | float) and isinstance(pred, int | float):
        return calculate_number_score(pred, gold)
    elif isinstance(gold, str) and isinstance(pred, str):
        return compute_f1(pred, gold)
    elif isinstance(gold, list) and isinstance(pred, list):
        if all(isinstance(item, str) for item in gold + pred):
            return compute_f1(" ".join(pred), " ".join(gold))
        elif all(isinstance(item, int | float) for item in gold + pred):
            return compute_f1(
                " ".join([str(x) for x in pred]), " ".join([str(x) for x in gold])
            )
        elif all(isinstance(item, dict) for item in gold + pred):
            return sum(calculate_json_score(p, g) for p, g in zip(pred, gold)) / max(
                len(pred), len(gold)
            )
    elif isinstance(gold, dict) and isinstance(pred, dict):
        return calculate_json_score(pred, gold)

    return 0.0  # Mismatched types
