"""
pack_brain.py - Palette/pack selection focused on coherent reggaeton production.

Builds candidate palettes from the local library by scoring folder-level coherence
across drums, bass, music, vocal and FX material. The goal is to stop selecting
good isolated samples that do not belong to the same sonic universe.
"""

from __future__ import annotations

import itertools
import logging
import re
from collections import Counter, defaultdict
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple

logger = logging.getLogger("PackBrain")

IGNORED_SEGMENTS = {
    "(extra)",
    ".sample_cache",
    ".segment_rag",
    "__pycache__",
    "documentation",
    "installer",
    "flp",
}

GENERIC_FOLDER_HINTS = {
    "kick",
    "snare",
    "drumloops",
    "drumloop",
    "oneshots",
    "one shots",
    "fx",
    "bass",
    "perc loop",
    "perc",
    "sounds presets",
    "sample pack",
    "drum loops",
    "instrumental loops",
    "vocal phrases",
    "music loops",
    "one shots",
    "hi hat",
    "hi-hat",
}

BUS_ROLE_KEYWORDS = {
    "drums": {
        "kick", "snare", "clap", "hat", "hihat", "drum", "dembow", "perc",
        "percussion", "shaker", "loop", "drumloop", "toploop", "ride",
    },
    "bass": {"bass", "sub", "808", "reese"},
    "music": {
        "music", "instrumental", "synth", "lead", "pluck", "arp", "pad",
        "melody", "melodic", "keys", "piano", "guitar", "loop", "hook",
    },
    "vocal": {"vocal", "vox", "phrase", "double", "harmony", "libs", "choir"},
    "fx": {"fx", "impact", "riser", "fill", "sweep", "transition", "reverse", "atmos"},
}

ROLE_TO_BUS = {
    "kick": "drums",
    "snare": "drums",
    "clap": "drums",
    "hat": "drums",
    "perc": "drums",
    "top_loop": "drums",
    "perc_loop": "drums",
    "bass": "bass",
    "sub": "bass",
    "bass_loop": "bass",
    "synth": "music",
    "synth_loop": "music",
    "synth_peak": "music",
    "instrumental": "music",
    "vocal": "vocal",
    "vocal_loop": "vocal",
    "vocal_peak": "vocal",
    "vocal_build": "vocal",
    "vocal_shot": "vocal",
    "fx": "fx",
    "fill_fx": "fx",
    "crash_fx": "fx",
    "atmos_fx": "fx",
    "snare_roll": "fx",
}

STOP_TOKENS = {
    "wav", "mp3", "flac", "aiff", "aif", "loop", "loops", "shot", "shots", "one",
    "audio", "pack", "sample", "samples", "prod", "the", "and", "with", "para",
    "todos", "usan", "este", "type", "main", "latin", "latinos",
}

NOTE_TO_SEMITONE = {
    "c": 0,
    "c#": 1,
    "db": 1,
    "d": 2,
    "d#": 3,
    "eb": 3,
    "e": 4,
    "f": 5,
    "f#": 6,
    "gb": 6,
    "g": 7,
    "g#": 8,
    "ab": 8,
    "a": 9,
    "a#": 10,
    "bb": 10,
    "b": 11,
}

ENHARMONIC_EQUIV = {
    "db": "c#",
    "eb": "d#",
    "gb": "f#",
    "ab": "g#",
    "bb": "a#",
}


def _tokenize(text: str) -> List[str]:
    cleaned = re.sub(r"[^a-z0-9#]+", " ", str(text or "").lower())
    return [token for token in cleaned.split() if len(token) > 1 and token not in STOP_TOKENS]


def _extract_bpm(text: str) -> Optional[float]:
    match = re.search(r"(?<!\d)(\d{2,3})(?:\s?bpm|\s?bpms)?(?!\d)", str(text or "").lower())
    if not match:
        return None
    value = float(match.group(1))
    if 60.0 <= value <= 180.0:
        return value
    return None


def _normalize_key(value: Any) -> str:
    text = str(value or "").strip().lower()
    if not text:
        return ""
    text = text.replace("minor", "m").replace("major", "")
    text = text.replace(" min", "m").replace(" maj", "")
    text = text.replace("_", "").replace("-", "").replace(" ", "")
    text = text.replace("♭", "b").replace("♯", "#")
    mode = "m" if text.endswith("m") else ""
    note = text[:-1] if mode else text
    note = ENHARMONIC_EQUIV.get(note, note)
    return f"{note}{mode}"


def _split_key(value: Any) -> Tuple[str, str]:
    normalized = _normalize_key(value)
    if not normalized:
        return "", ""
    if normalized.endswith("m"):
        return normalized[:-1], "minor"
    return normalized, "major"


def _extract_key(text: str) -> str:
    lowered = str(text or "").lower()
    patterns = [
        r"([a-g])([#b]?)[ _-]?(?:min|minor|m)(?:\b|_)",
        r"([a-g])([#b]?)[ _-]?(?:maj|major)(?:\b|_)",
        r"\b([a-g])([#b]?m)(?:\b|_)",
        r"\b([a-g])([#b]?)\b",
    ]
    for pattern in patterns:
        match = re.search(pattern, lowered)
        if not match:
            continue
        if len(match.groups()) == 2:
            return _normalize_key("".join(match.groups()))
        return _normalize_key("".join(match.groups()))
    return ""


def _key_score(target_key: str, candidate_key: str) -> float:
    target = _normalize_key(target_key)
    candidate = _normalize_key(candidate_key)
    if not target or not candidate:
        return 0.55
    if target == candidate:
        return 1.0

    target_note, target_mode = _split_key(target)
    candidate_note, candidate_mode = _split_key(candidate)
    target_pc = NOTE_TO_SEMITONE.get(target_note)
    candidate_pc = NOTE_TO_SEMITONE.get(candidate_note)
    if target_pc is None or candidate_pc is None:
        return 0.55

    if target_note == candidate_note and target_mode != candidate_mode:
        return 0.78

    if target_mode != candidate_mode:
        if target_mode == "major" and ((target_pc + 9) % 12) == candidate_pc:
            return 0.9
        if target_mode == "minor" and ((target_pc + 3) % 12) == candidate_pc:
            return 0.9

    distance = min((target_pc - candidate_pc) % 12, (candidate_pc - target_pc) % 12)
    if distance in {5, 7} and target_mode == candidate_mode:
        return 0.72
    if distance in {2, 10} and target_mode == candidate_mode:
        return 0.54
    if distance in {3, 4}:
        return 0.38
    return 0.24


def _shared_token_bonus(groups: Sequence[Sequence[str]]) -> Tuple[float, List[str]]:
    counters = [Counter(tokens) for tokens in groups if tokens]
    if not counters:
        return 0.0, []
    intersection = set(counters[0].keys())
    for counter in counters[1:]:
        intersection &= set(counter.keys())
    shared = sorted(token for token in intersection if token not in STOP_TOKENS)
    bonus = min(2.4, 0.35 * len(shared))
    return bonus, shared[:8]


@dataclass
class FolderStats:
    path: str
    bus: str
    sample_count: int = 0
    loop_count: int = 0
    one_shot_count: int = 0
    bpm_values: List[float] = field(default_factory=list)
    keys: Counter = field(default_factory=Counter)
    tokens: Counter = field(default_factory=Counter)
    source_roots: Counter = field(default_factory=Counter)

    def to_summary(self) -> Dict[str, Any]:
        dominant_key = self.keys.most_common(1)[0][0] if self.keys else ""
        avg_bpm = round(sum(self.bpm_values) / len(self.bpm_values), 2) if self.bpm_values else None
        return {
            "path": self.path,
            "bus": self.bus,
            "sample_count": self.sample_count,
            "loop_count": self.loop_count,
            "one_shot_count": self.one_shot_count,
            "avg_bpm": avg_bpm,
            "dominant_key": dominant_key,
            "top_tokens": [token for token, _ in self.tokens.most_common(8)],
            "source_root": self.source_roots.most_common(1)[0][0] if self.source_roots else "",
        }


class PackBrain:
    """Derive coherent palettes from the user's library."""

    def __init__(self, manager: Any):
        self.manager = manager
        self.base_dir = Path(getattr(manager, "base_dir", "."))
        self._folder_stats: Dict[Tuple[str, str], FolderStats] = {}
        self._prepared = False

    def _should_ignore(self, sample_path: Path) -> bool:
        return any(part.strip().lower() in IGNORED_SEGMENTS for part in sample_path.parts)

    def _detect_bus(self, sample: Any, sample_path: Path) -> str:
        haystack = " ".join(
            [
                sample_path.as_posix().lower(),
                str(getattr(sample, "category", "")).lower(),
                str(getattr(sample, "subcategory", "")).lower(),
                str(getattr(sample, "sample_type", "")).lower(),
            ]
        )
        bus_scores = {}
        for bus, keywords in BUS_ROLE_KEYWORDS.items():
            bus_scores[bus] = sum(1 for keyword in keywords if keyword in haystack)
        if "vocal" in haystack or "vox" in haystack:
            bus_scores["vocal"] += 2
        if "fx" in haystack or "impact" in haystack or "transition" in haystack:
            bus_scores["fx"] += 2
        best_bus, best_score = max(bus_scores.items(), key=lambda item: item[1])
        return best_bus if best_score > 0 else "music"

    def _source_root(self, relative_parts: Sequence[str]) -> str:
        for part in relative_parts:
            lowered = part.strip().lower()
            if lowered not in GENERIC_FOLDER_HINTS and lowered not in STOP_TOKENS:
                return part
        return relative_parts[0] if relative_parts else "library"

    def _build_stats(self) -> None:
        if self._prepared:
            return

        for sample in getattr(self.manager, "samples", {}).values():
            sample_path = Path(str(getattr(sample, "path", "") or ""))
            if not sample_path.is_file() or self._should_ignore(sample_path):
                continue
            try:
                rel = sample_path.relative_to(self.base_dir)
                rel_parts = rel.parts[:-1]
            except ValueError:
                rel_parts = sample_path.parts[:-1]
            bus = self._detect_bus(sample, sample_path)
            folder_key = (bus, str(sample_path.parent))
            stats = self._folder_stats.setdefault(folder_key, FolderStats(path=str(sample_path.parent), bus=bus))
            stats.sample_count += 1

            sample_name = str(getattr(sample, "name", sample_path.stem))
            duration = float(getattr(sample, "duration", 0.0) or 0.0)
            bpm = getattr(sample, "bpm", None) or _extract_bpm(sample_name) or _extract_bpm(sample_path.as_posix())
            key = getattr(sample, "key", None) or _extract_key(sample_name) or _extract_key(sample_path.as_posix())
            if bpm:
                stats.bpm_values.append(float(bpm))
            if key:
                stats.keys[_normalize_key(key)] += 1

            looks_like_loop = duration >= 1.25 or "loop" in sample_name.lower() or "loop" in sample_path.as_posix().lower()
            if looks_like_loop:
                stats.loop_count += 1
            else:
                stats.one_shot_count += 1

            token_source = " ".join(list(rel_parts) + [sample_name])
            stats.tokens.update(_tokenize(token_source))
            stats.source_roots[self._source_root(rel_parts)] += 1

        self._prepared = True

    def _folder_request_score(self, stats: FolderStats, genre: str, style: str, bpm: float, key: str) -> Tuple[float, List[str]]:
        score = 0.0
        reasons: List[str] = []
        tokens = {token for token, _ in stats.tokens.most_common(20)}
        request_tokens = set(_tokenize(f"{genre} {style}"))
        folder_text = Path(stats.path).as_posix().lower()

        if stats.sample_count:
            density_bonus = min(2.2, 0.2 * stats.sample_count)
            score += density_bonus
            reasons.append(f"{stats.sample_count} samples")

        if stats.loop_count and stats.bus in {"drums", "music", "vocal"}:
            loop_bonus = min(1.6, 0.25 * stats.loop_count)
            score += loop_bonus
        if stats.one_shot_count and stats.bus in {"drums", "bass"}:
            one_shot_bonus = min(1.2, 0.2 * stats.one_shot_count)
            score += one_shot_bonus

        if request_tokens:
            overlap = request_tokens & tokens
            if overlap:
                score += 0.6 * len(overlap)
                reasons.append(f"keywords {sorted(overlap)}")

        if "reggaeton" in " ".join(tokens) or "dembow" in " ".join(tokens):
            score += 1.1

        if stats.bus == "drums":
            if any(term in folder_text for term in ["/drum", "/kick", "/snare", "/oneshot", "drum loops", "drumloops"]):
                score += 1.4
            if "/fx/" in folder_text or "fill" in folder_text:
                score -= 0.9
        elif stats.bus == "bass":
            if "/bass/" in folder_text or " sub" in folder_text or "/sub" in folder_text:
                score += 1.6
            if "/fx/" in folder_text or "fill" in folder_text or "impact" in folder_text:
                score -= 1.8
        elif stats.bus == "music":
            if "instrumental loops" in folder_text or "music loops" in folder_text or "sample pack" in folder_text:
                score += 1.6
            if "/fx/" in folder_text or "fill" in folder_text or "drum loop" in folder_text:
                score -= 1.4
        elif stats.bus == "vocal":
            if "vocal" in folder_text or "vox" in folder_text or "phrases" in folder_text:
                score += 1.4
        elif stats.bus == "fx":
            if "/fx/" in folder_text or "fill" in folder_text or "impact" in folder_text or "transition" in folder_text:
                score += 1.4

        if bpm > 0 and stats.bpm_values:
            avg_bpm = sum(stats.bpm_values) / len(stats.bpm_values)
            diff = abs(avg_bpm - bpm)
            if diff <= 1.5:
                score += 2.4
                reasons.append(f"BPM {avg_bpm:.1f}")
            elif diff <= 4:
                score += 1.8
            elif diff <= 8:
                score += 1.0
            elif abs(avg_bpm - (bpm * 2.0)) <= 4 or abs(avg_bpm - (bpm / 2.0)) <= 3:
                score += 0.75

        if key and stats.keys:
            dominant_key = stats.keys.most_common(1)[0][0]
            compatibility = _key_score(key, dominant_key)
            score += compatibility * 2.2
            if compatibility >= 0.8:
                reasons.append(f"key {dominant_key}")

        source_root = stats.source_roots.most_common(1)[0][0] if stats.source_roots else ""
        if source_root and source_root.lower() not in GENERIC_FOLDER_HINTS:
            score += 0.5

        return score, reasons

    def _support_folder_score(
        self,
        stats: FolderStats,
        requested_bus: str,
        palette_tokens: Sequence[Sequence[str]],
        genre: str,
        style: str,
        bpm: float,
        key: str,
    ) -> float:
        base_score, _ = self._folder_request_score(stats, genre, style, bpm, key)
        bus_bonus = 1.2 if stats.bus == requested_bus else 0.0
        shared_bonus, _ = _shared_token_bonus(list(palette_tokens) + [[token for token, _ in stats.tokens.most_common(10)]])
        return base_score + bus_bonus + shared_bonus

    def rank_palettes(
        self,
        genre: str,
        style: str = "",
        bpm: float = 0.0,
        key: str = "",
        max_candidates: int = 5,
    ) -> Dict[str, Any]:
        self._build_stats()

        bus_rankings: Dict[str, List[Tuple[float, FolderStats, List[str]]]] = defaultdict(list)
        for (_, _), stats in self._folder_stats.items():
            if stats.bus not in {"drums", "bass", "music", "vocal", "fx"}:
                continue
            folder_score, reasons = self._folder_request_score(stats, genre, style, bpm, key)
            if folder_score <= 0:
                continue
            bus_rankings[stats.bus].append((folder_score, stats, reasons))

        for bus in bus_rankings:
            bus_rankings[bus].sort(key=lambda item: item[0], reverse=True)

        drums = bus_rankings.get("drums", [])[:4]
        bass = bus_rankings.get("bass", [])[:4]
        music = bus_rankings.get("music", [])[:4]
        vocals = bus_rankings.get("vocal", [])[:4]
        fxs = bus_rankings.get("fx", [])[:4]

        palette_candidates: List[Dict[str, Any]] = []
        candidate_index = 0

        for drums_item, bass_item, music_item in itertools.product(drums or [None], bass or [None], music or [None]):
            if not drums_item or not bass_item or not music_item:
                continue
            selected = [drums_item[1], bass_item[1], music_item[1]]
            token_groups = [[token for token, _ in stats.tokens.most_common(10)] for stats in selected]
            shared_bonus, shared_tokens = _shared_token_bonus(token_groups)
            source_roots = [
                stats.source_roots.most_common(1)[0][0]
                for stats in selected
                if stats.source_roots
            ]
            source_counter = Counter(source_roots)
            source_bonus = 0.0
            if source_counter:
                most_common_source, source_hits = source_counter.most_common(1)[0]
                if source_hits >= 3:
                    source_bonus += 2.2
                elif source_hits == 2:
                    source_bonus += 1.4
                if most_common_source.lower() in {"reggaeton 3", "sentimientolatino2025"}:
                    source_bonus += 0.4
            if Path(bass_item[1].path).parent == Path(music_item[1].path).parent:
                source_bonus += 1.6

            harmony_notes: List[str] = []
            bass_key = bass_item[1].keys.most_common(1)[0][0] if bass_item[1].keys else ""
            music_key = music_item[1].keys.most_common(1)[0][0] if music_item[1].keys else ""
            harmony_score = _key_score(bass_key, music_key) if bass_key and music_key else 0.55
            if bass_key and music_key:
                if harmony_score >= 0.9:
                    source_bonus += 1.8
                    harmony_notes.append(f"harmonic lock {bass_key}/{music_key}")
                elif harmony_score >= 0.72:
                    source_bonus += 0.9
                    harmony_notes.append(f"harmonic fit {bass_key}/{music_key}")
                elif harmony_score >= 0.54:
                    source_bonus += 0.2
                    harmony_notes.append(f"harmonic risk {bass_key}/{music_key}")
                else:
                    source_bonus -= 3.5
                    harmony_notes.append(f"harmonic clash {bass_key}/{music_key}")

            palette_score = drums_item[0] + bass_item[0] + music_item[0] + shared_bonus + source_bonus
            reason_bits = list(dict.fromkeys(harmony_notes + drums_item[2] + bass_item[2] + music_item[2]))

            palette = {
                "drums": drums_item[1].path,
                "bass": bass_item[1].path,
                "music": music_item[1].path,
            }

            support_folders: Dict[str, str] = {}
            for bus_name, support_rankings in (("vocal", vocals), ("fx", fxs)):
                if not support_rankings:
                    continue
                best_support = max(
                    support_rankings,
                    key=lambda item: self._support_folder_score(
                        item[1], bus_name, token_groups, genre, style, bpm, key
                    ),
                )
                support_folders[bus_name] = best_support[1].path

            if support_folders:
                palette_score += 0.35 * len(support_folders)

            candidate_index += 1
            palette_candidates.append(
                {
                    "id": f"palette-{candidate_index}",
                    "score": round(palette_score, 3),
                    "harmony_score": round(harmony_score, 3),
                    "harmony_verdict": (
                        "compatible" if harmony_score >= 0.72
                        else "risky" if harmony_score >= 0.54
                        else "clash"
                    ),
                    "palette": palette,
                    "support_folders": support_folders,
                    "shared_tokens": shared_tokens,
                    "reasons": reason_bits[:10],
                    "folders": {
                        "drums": drums_item[1].to_summary(),
                        "bass": bass_item[1].to_summary(),
                        "music": music_item[1].to_summary(),
                        "vocal": next((item[1].to_summary() for item in vocals if item[1].path == support_folders.get("vocal")), None),
                        "fx": next((item[1].to_summary() for item in fxs if item[1].path == support_folders.get("fx")), None),
                    },
                }
            )

        palette_candidates.sort(key=lambda item: item["score"], reverse=True)
        selected = palette_candidates[0] if palette_candidates else {}
        return {
            "genre": genre,
            "style": style,
            "bpm": bpm,
            "key": key,
            "selected_palette": selected,
            "candidates": palette_candidates[:max_candidates],
            "folder_rankings": {
                bus: [
                    {
                        "score": round(score, 3),
                        "summary": stats.to_summary(),
                        "reasons": reasons[:6],
                    }
                    for score, stats, reasons in rankings[:max_candidates]
                ]
                for bus, rankings in bus_rankings.items()
            },
        }

    def get_folder_compatibility_score(self, folder1: str, folder2: str) -> Tuple[float, str]:
        """
        Calculate compatibility score between two folders.
        
        Returns:
            Tuple of (score, relationship_type)
            - score: 0.0 to 1.0 compatibility score
            - relationship_type: 'exact', 'sibling', 'cousin', 'unrelated'
        """
        import os
        f1 = folder1.replace(os.sep, '/')
        f2 = folder2.replace(os.sep, '/')
        
        # Exact same folder
        if f1 == f2:
            return 1.0, 'exact'
        
        p1 = str(Path(f1).parent).replace(os.sep, '/')
        p2 = str(Path(f2).parent).replace(os.sep, '/')
        
        # Sibling folders (same parent)
        if p1 == p2:
            return 0.85, 'sibling'
        
        gp1 = str(Path(p1).parent).replace(os.sep, '/') if p1 else ''
        gp2 = str(Path(p2).parent).replace(os.sep, '/') if p2 else ''
        
        # Cousin folders (same grandparent)
        if gp1 == gp2 and gp1:
            return 0.70, 'cousin'
        
        # Check if folders share tokens
        tokens1 = set(_tokenize(f1))
        tokens2 = set(_tokenize(f2))
        shared = tokens1 & tokens2
        
        if shared:
            # Shared tokens indicate some relationship
            return 0.55, 'related'
        
        return 0.30, 'unrelated'
    
    def evaluate_folder_combination(self, folders: Dict[str, str]) -> Dict[str, Any]:
        """
        Evaluate a combination of folders for different buses/roles.
        
        Args:
            folders: Dict mapping bus/role to folder path
            
        Returns:
            Dict with compatibility analysis
        """
        if not folders or len(folders) < 2:
            return {
                'overall_score': 0.0,
                'pair_scores': {},
                'recommendation': 'Need at least 2 folders to evaluate'
            }
        
        pair_scores = {}
        total_score = 0.0
        pair_count = 0
        
        items = list(folders.items())
        for i, (role1, folder1) in enumerate(items):
            for role2, folder2 in items[i+1:]:
                score, relationship = self.get_folder_compatibility_score(folder1, folder2)
                pair_key = f"{role1}-{role2}"
                pair_scores[pair_key] = {
                    'score': round(score, 3),
                    'relationship': relationship,
                    'folder1': Path(folder1).name,
                    'folder2': Path(folder2).name,
                }
                total_score += score
                pair_count += 1
        
        overall_score = total_score / pair_count if pair_count > 0 else 0.0
        
        # Generate recommendation
        if overall_score >= 0.8:
            recommendation = "Excellent folder combination - highly coherent"
        elif overall_score >= 0.6:
            recommendation = "Good folder combination - reasonably coherent"
        elif overall_score >= 0.4:
            recommendation = "Moderate coherence - some folders are unrelated"
        else:
            recommendation = "Poor coherence - folders are from different packs/sources"
        
        return {
            'overall_score': round(overall_score, 3),
            'pair_scores': pair_scores,
            'folder_count': len(folders),
            'recommendation': recommendation,
        }
    
    def find_compatible_folder_for_role(self, 
                                        target_role: str,
                                        reference_folders: List[str],
                                        genre: str = "",
                                        bpm: float = 0,
                                        key: str = "") -> Optional[str]:
        """
        Find a folder for a role that is compatible with reference folders.
        
        Args:
            target_role: Role to find folder for (e.g., 'fill_fx', 'snare_roll')
            reference_folders: List of reference folder paths to match against
            genre: Genre for filtering
            bpm: BPM for filtering
            key: Key for filtering
            
        Returns:
            Best matching folder path or None
        """
        self._build_stats()
        
        # Determine bus for role
        target_bus = ROLE_TO_BUS.get(target_role, 'fx')
        
        # Get candidate folders for this bus
        candidates = []
        for (bus, path), stats in self._folder_stats.items():
            if bus == target_bus:
                score, _ = self._folder_request_score(stats, genre, "", bpm, key)
                if score > 0:
                    candidates.append((score, path, stats))
        
        if not candidates:
            return None
        
        # Score candidates by compatibility with reference folders
        scored_candidates = []
        for base_score, path, stats in candidates:
            compatibility_bonus = 0.0
            for ref_folder in reference_folders:
                compat_score, _ = self.get_folder_compatibility_score(path, ref_folder)
                compatibility_bonus += compat_score * 0.5
            
            final_score = base_score + compatibility_bonus
            scored_candidates.append((final_score, path))
        
        scored_candidates.sort(reverse=True)
        
        if scored_candidates:
            best_folder = scored_candidates[0][1]
            logger.debug("COMPAT_FOLDER [%s]: Selected '%s' with score %.2f (matched against %d refs)",
                        target_role, Path(best_folder).name, scored_candidates[0][0], 
                        len(reference_folders))
            return best_folder
        
        return None
