from __future__ import annotations

from typing import Optional, List, Dict, Set
import re

from .. import builder_constants as bc


class LandTripleMixin:
    """Mixin providing logic for adding three-color (triple) lands (Step 6).

    Extraction rationale:
      - Isolates a coherent land selection concern from the monolithic builder.
      - Mirrors earlier land step mixins with add_* and run_land_step6 methods.

    Strategy:
      1. Determine if the deck's color identity has at least 3 colors; else skip.
      2. Build a pool of candidate triple lands whose type line / name indicates they
         produce at least three of the deck colors (heuristic; full rules parsing is
         intentionally avoided for speed / simplicity with CSV data).
      3. Avoid adding duplicates or previously selected lands.
      4. Trim basics (above a computed floor) if capacity is reached and we still
         desire triple lands.
      5. Respect user-provided requested_count if supplied; otherwise fall back to
         default constant and capacity.
      6. Apply a simple ranking + slight randomization for determinism + variety.
    """

    def add_triple_lands(self, requested_count: Optional[int] = None):
        # Preconditions: color identity & dataframes
        if not getattr(self, 'files_to_load', None):
            try:
                self.determine_color_identity()
                self.setup_dataframes()
            except Exception as e:  # pragma: no cover - defensive
                self.output_func(f"Cannot add triple lands until setup complete: {e}")
                return

        colors = [c for c in getattr(self, 'color_identity', []) if c in ['W','U','B','R','G']]
        if len(colors) < 3:
            self.output_func("Triple Lands: Fewer than three colors; skipping step 6.")
            return

        land_target = getattr(self, 'ideal_counts', {}).get('lands', getattr(bc, 'DEFAULT_LAND_COUNT', 35)) if getattr(self, 'ideal_counts', None) else getattr(bc, 'DEFAULT_LAND_COUNT', 35)

        df = getattr(self, '_combined_cards_df', None)
        if df is None or df.empty or not {'name','type'}.issubset(df.columns):
            self.output_func("Triple Lands: No combined card dataframe or missing columns; skipping.")
            return

        pool: List[str] = []
        meta: Dict[str, str] = {}
        wanted: Set[str] = set(colors)
        basic_map = {
            'plains': 'W',
            'island': 'U',
            'swamp': 'B',
            'mountain': 'R',
            'forest': 'G',
        }

        for _, row in df.iterrows():  # type: ignore
            try:
                name = str(row.get('name',''))
                if not name or name in self.card_library:
                    continue
                tline = str(row.get('type','')).lower()
                if 'land' not in tline:
                    continue
                # Heuristic: count unique basic types in type line
                types_present = [b for b in basic_map if b in tline]
                mapped = {basic_map[b] for b in types_present}

                # Extract color production from rules text if present
                text_field = str(row.get('text', row.get('oracleText',''))).lower()
                color_syms = set(re.findall(r'\{([wubrg])\}', text_field))
                color_syms_mapped = {c.upper() for c in color_syms}

                lname = name.lower()
                tri_keywords = [
                    'triome','panorama','citadel','tower','hub','garden','headquarters','sanctuary',
                    'stronghold','outpost','campus','shrine','domain','estate'
                ]

                # Decide if candidate qualifies:
                qualifies_by_types = len(mapped) >= 3
                qualifies_by_text = len(color_syms_mapped) >= 3 and color_syms_mapped.issubset(wanted)
                qualifies_by_name = any(kw in lname for kw in tri_keywords)

                if not (qualifies_by_types or qualifies_by_text or (qualifies_by_name and (len(mapped) >= 2 or len(color_syms_mapped) >= 2))):
                    continue

                # Consolidate produced colors for validation (prefer typed, else text)
                produced = mapped if mapped else color_syms_mapped
                if not produced.issubset(wanted):
                    continue

                if qualifies_by_types or len(produced) >= 3:
                    pool.append(name)
                    meta[name] = tline
                else:
                    pool.append(name)
                    meta[name] = tline + ' (heuristic-tri)'
            except Exception:  # pragma: no cover - defensive
                continue

        # De-duplicate while preserving order
        pool = list(dict.fromkeys(pool))
        if not pool:
            self.output_func("Triple Lands: No candidates found.")
            return

        # Ranking heuristic: fully triple-typed > untapped > others; penalize ETB tapped
        def rank(name: str) -> int:
            tline = meta.get(name, '')
            score = 0
            if '(heuristic-tri)' not in tline:
                score += 5
            if 'enters the battlefield tapped' not in tline:
                score += 2
            if 'cycling' in tline:
                score += 1
            if 'enters the battlefield tapped' in tline and 'you gain' in tline:
                score -= 1
            return score

        pool.sort(key=lambda n: rank(n), reverse=True)

        # Slight randomized shuffle weighted by rank for variety
        rng = getattr(self, 'rng', None) or self._get_rng()
        try:
            weighted = []
            for n in pool:
                w = max(1, rank(n)) + 1
                weighted.append((n, w))
            shuffled: List[str] = []
            while weighted:
                total = sum(w for _, w in weighted)
                r = rng.random() * total
                acc = 0.0
                for idx, (n, w) in enumerate(weighted):
                    acc += w
                    if r <= acc:
                        shuffled.append(n)
                        del weighted[idx]
                        break
            pool = shuffled
        except Exception:  # pragma: no cover - fallback
            pass

        # Capacity handling
        remaining_capacity = max(0, land_target - self._current_land_count())
        default_triple_target = getattr(bc, 'TRIPLE_LAND_DEFAULT_COUNT', 3)
        effective_default = min(default_triple_target, remaining_capacity if remaining_capacity>0 else len(pool), len(pool))
        if requested_count is None:
            desired = effective_default
        else:
            desired = max(0, int(requested_count))
        if desired == 0:
            self.output_func("Triple Lands: Desired count 0; skipping.")
            return

        min_basic_cfg = getattr(bc, 'DEFAULT_BASIC_LAND_COUNT', 20)
        if hasattr(self, 'ideal_counts') and self.ideal_counts:
            min_basic_cfg = self.ideal_counts.get('basic_lands', min_basic_cfg)
        basic_floor = self._basic_floor(min_basic_cfg)

        if remaining_capacity == 0 and desired > 0:
            slots_needed = desired
            freed = 0
            while freed < slots_needed and self._count_basic_lands() > basic_floor:
                target_basic = self._choose_basic_to_trim()
                if not target_basic:
                    break
                if not self._decrement_card(target_basic):
                    break
                freed += 1
            if freed == 0:
                desired = 0
        remaining_capacity = max(0, land_target - self._current_land_count())
        desired = min(desired, remaining_capacity, len(pool))
        if desired <= 0:
            self.output_func("Triple Lands: No capacity after trimming; skipping.")
            return

        added: List[str] = []
        for name in pool:
            if len(added) >= desired or self._current_land_count() >= land_target:
                break
            # Infer color trio from type line basic types
            try:
                row_match = df[df['name'] == name]
                tline = ''
                text_field = ''
                sub_role = None
                if not row_match.empty:
                    rw = row_match.iloc[0]
                    tline = str(rw.get('type','')).lower()
                    text_field = str(rw.get('text', rw.get('oracleText',''))).lower()
                trio = []
                for basic, col in [('plains','W'),('island','U'),('swamp','B'),('mountain','R'),('forest','G')]:
                    if basic in tline:
                        trio.append(col)
                if len(trio) < 3:
                    color_syms = set(re.findall(r'\{([wubrg])\}', text_field))
                    trio = [c.upper() for c in color_syms]
                if len(trio) >= 2:
                    sub_role = ''.join(sorted(set(trio)))
            except Exception:
                sub_role = None
            self.add_card(
                name,
                card_type='Land',
                role='triple',
                sub_role=sub_role,
                added_by='lands_step6'
            )
            added.append(name)


        self.output_func("\nTriple Lands Added (Step 6):")
        if not added:
            self.output_func("  (None added)")
        else:
            width = max(len(n) for n in added)
            for n in added:
                self.output_func(f"  {n.ljust(width)} : 1")
        self.output_func(f"  Land Count Now : {self._current_land_count()} / {land_target}")

    def run_land_step6(self, requested_count: Optional[int] = None):
        self.add_triple_lands(requested_count=requested_count)
        self._enforce_land_cap(step_label="Triples (Step 6)")
        try:
            from .. import builder_utils as _bu
            _bu.export_current_land_pool(self, '6')
        except Exception:
            pass
