import numpy as np
from typing import List, Dict, Any, Optional
from scipy.spatial.distance import cosine, euclidean, cityblock, hamming
from sklearn.metrics import jaccard_score


def cosine_similarity(vec1: List[float], vec2: List[float]) -> float:
    """Calculate cosine similarity between two vectors"""
    if len(vec1) == 0 or len(vec2) == 0:
        raise ValueError("Vectors must not be empty")
    if len(vec1) != len(vec2):
        raise ValueError("Vectors must have the same length")

    return 1 - cosine(vec1, vec2)


def euclidean_distance(vec1: List[float], vec2: List[float]) -> float:
    """Calculate Euclidean distance between two vectors"""
    if len(vec1) == 0 or len(vec2) == 0:
        raise ValueError("Vectors must not be empty")
    if len(vec1) != len(vec2):
        raise ValueError("Vectors must have the same length")

    return float(euclidean(vec1, vec2))


def manhattan_distance(vec1: List[float], vec2: List[float]) -> float:
    """Calculate Manhattan distance between two vectors"""
    if len(vec1) == 0 or len(vec2) == 0:
        raise ValueError("Vectors must not be empty")
    if len(vec1) != len(vec2):
        raise ValueError("Vectors must have the same length")

    return float(cityblock(vec1, vec2))


def jaccard_similarity(set1: List[Any], set2: List[Any]) -> float:
    """Calculate Jaccard similarity between two sets"""
    if len(set1) == 0 or len(set2) == 0:
        return 0.0

    set1 = np.array(set1)
    set2 = np.array(set2)

    if set1.ndim == 1:
        set1 = set1.reshape(1, -1)
    if set2.ndim == 1:
        set2 = set2.reshape(1, -1)

    return float(jaccard_score(set1, set2, average="micro"))


def hamming_distance(str1: str, str2: str) -> int:
    """Calculate Hamming distance between two strings"""
    if len(str1) != len(str2):
        raise ValueError("Strings must have the same length")

    return sum(c1 != c2 for c1, c2 in zip(str1, str2))


def cosine_similarity_matrix(vectors: List[List[float]]) -> np.ndarray:
    """Calculate cosine similarity matrix for a list of vectors"""
    if len(vectors) == 0:
        raise ValueError("Vectors list must not be empty")

    vectors = np.array(vectors)
    norms = np.linalg.norm(vectors, axis=1, keepdims=True)
    normalized = vectors / np.where(norms == 0, 1, norms)

    return np.dot(normalized, normalized.T)


def batch_cosine_similarity(vectors1: List[List[float]], vectors2: List[List[float]]) -> np.ndarray:
    """Calculate cosine similarity between two batches of vectors"""
    if len(vectors1) == 0 or len(vectors2) == 0:
        raise ValueError("Vectors lists must not be empty")

    vectors1 = np.array(vectors1)
    vectors2 = np.array(vectors2)

    norms1 = np.linalg.norm(vectors1, axis=1, keepdims=True)
    norms2 = np.linalg.norm(vectors2, axis=1, keepdims=True)

    normalized1 = vectors1 / np.where(norms1 == 0, 1, norms1)
    normalized2 = vectors2 / np.where(norms2 == 0, 1, norms2)

    return np.dot(normalized1, normalized2.T)


def weighted_similarity(
    vec1: List[float],
    vec2: List[float],
    weights: Optional[List[float]] = None
) -> float:
    """Calculate weighted cosine similarity"""
    if len(vec1) == 0 or len(vec2) == 0:
        raise ValueError("Vectors must not be empty")
    if len(vec1) != len(vec2):
        raise ValueError("Vectors must have the same length")
    if weights and len(vec1) != len(weights):
        raise ValueError("Vectors and weights must have the same length")

    if weights is None:
        return cosine_similarity(vec1, vec2)

    weighted_vec1 = np.array(vec1) * np.array(weights)
    weighted_vec2 = np.array(vec2) * np.array(weights)

    return cosine_similarity(weighted_vec1.tolist(), weighted_vec2.tolist())


def similarity_with_threshold(
    vec1: List[float],
    vec2: List[float],
    threshold: float = 0.5
) -> Dict[str, Any]:
    """Calculate similarity with threshold check"""
    similarity = cosine_similarity(vec1, vec2)

    return {
        "similarity": similarity,
        "is_similar": similarity >= threshold,
        "threshold": threshold
    }


def find_most_similar(
    query_vector: List[float],
    candidate_vectors: List[List[float]],
    top_k: int = 1
) -> List[Dict[str, Any]]:
    """Find the most similar vectors to the query vector"""
    if len(candidate_vectors) == 0:
        return []

    similarities = batch_cosine_similarity([query_vector], candidate_vectors)[0]
    sorted_indices = np.argsort(-similarities)

    results = []
    for idx in sorted_indices[:top_k]:
        results.append({
            "index": int(idx),
            "similarity": float(similarities[idx]),
            "vector": candidate_vectors[idx]
        })

    return results