from typing import Any, Dict, Tuple
import torch
import torch.nn.functional as F
import numpy as np
from collections import defaultdict, Counter
import heapq

SAMPLE_RATE = 16000
WINDOW_SIZE = 512


def chunked(iterable, size):
    """Helper to yield successive chunks of given size from iterable."""
    for i in range(0, len(iterable), size):
        yield iterable[i : i + size]


class AudioFeatureExtractor:
    def __init__(
        self,
        sample_rate=SAMPLE_RATE,
        n_fft=WINDOW_SIZE,
        hop_length=None,
        fan_value=5,
        min_match_ratio: float = 0.1,
        device="cuda",
        kernal_size=5,
        min_prune_size=500,
        max_prune_ratio=0.8,
    ):
        """
        GPU-based Shazam with optional query‐match threshold.
        Args:
          …
          min_query_matches: only accept a match if vote count ≥ this threshold
        """
        self.sample_rate = sample_rate
        self.n_fft = n_fft
        self.hop_length = hop_length or n_fft // 4
        self.fan_value = fan_value
        self.min_match_ratio = min_match_ratio

        self.window = torch.hann_window(self.n_fft, device=device)
        self.min_hash_time_delta = 0
        self.max_hash_time_delta = 200
        self.min_hash_freq_delta = -30
        self.max_hash_freq_delta = 30
        self.kernal_size = kernal_size
        self.device = device
        self.min_prune_size = min_prune_size
        self.max_prune_ratio = max_prune_ratio

    def fingerprint(self, waveform, delta_compress: bool = False):
        """
        Compute fingerprints for a single audio waveform.

        Args:
            waveform (1D array-like): audio samples

        Returns:
            List of (hash, time_offset) tuples.
        """
        # Move signal to GPU
        sig = torch.tensor(waveform, dtype=torch.float32, device=self.device)

        # Compute complex STFT on GPU
        spec = torch.stft(
            sig,
            n_fft=self.n_fft,
            hop_length=self.hop_length,
            win_length=self.n_fft,
            window=self.window,
            center=False,
            return_complex=True,
        )

        # Magnitude spectrogram
        mag = spec.abs()

        # Local max pooling for peak detection
        padding = self.kernal_size // 2
        max_pooled = F.max_pool2d(
            mag.unsqueeze(0).unsqueeze(0),
            kernel_size=(self.kernal_size, self.kernal_size),
            stride=1,
            padding=(padding, padding),
        )

        # amp_min = torch.quantile(mag.flatten(), 0.99).item()
        amp_min = 10
        peaks = (mag.unsqueeze(0).unsqueeze(0) == max_pooled) & (mag.unsqueeze(0).unsqueeze(0) > amp_min)
        peaks = peaks.squeeze().cpu().numpy()

        freqs, times = np.where(peaks)
        # after you compute freqs, times:
        peak_list = list(zip(times, freqs))
        if len(peak_list) == 0:
            return []

        peak_list.sort(key=lambda x: x[0])  # sort by time
        times_sorted, freqs_sorted = zip(*peak_list)

        # now pair in temporal order
        hashes = []
        for i in range(len(times_sorted)):
            for j in range(1, self.fan_value):
                if i + j < len(times_sorted):
                    t1, f1 = times_sorted[i], freqs_sorted[i]
                    t2, f2 = times_sorted[i + j], freqs_sorted[i + j]
                    dt = t2 - t1
                    df = f2 - f1
                    if (
                        self.min_hash_time_delta <= dt <= self.max_hash_time_delta
                        and self.min_hash_freq_delta <= df <= self.max_hash_freq_delta
                    ):
                        h = (f1 << 24) | (f2 << 16) | dt
                        hashes.append((h, t1))

        if not delta_compress:
            return hashes

        # 3) aggregate into a list of (h, [t1, t2, …])
        grouping = defaultdict(list)
        for h, t in hashes:
            grouping[h].append(t)
        return [(h, grouping[h]) for h in grouping]
