import itertools
import hashlib
import random
from typing import List, Dict, Any, Set, Tuple

class NoveltyScorer:
    """
    A system for scoring the novelty of generated combinations.
    Novelty is a heuristic based on several factors:
    1. Number of elements combined (more elements can imply higher complexity/novelty).
    2. Rarity of individual elements (elements less frequently seen in prior art might contribute more).
    3. Conceptual distance (simulated here by a 'domain' attribute; combining elements from disparate domains).
    """
    @staticmethod
    def score(combination: Tuple[str, ...], prior_art_elements: Set[str], element_domains: Dict[str, str]) -> float:
        score = 0.0

        # Factor 1: Number of elements
        score += len(combination) * 1.0 # Each element adds 1 point

        # Factor 2: Rarity of individual elements (simplified: assume all prior_art_elements are 'common')
        # Here, we'll give a bonus for elements NOT found in a common prior art set
        rare_element_bonus = 0.5
        for element in combination:
            if element not in prior_art_elements:
                score += rare_element_bonus

        # Factor 3: Conceptual distance (simulated by domains)
        # Higher score if elements come from different 'domains'
        domains_in_combination = {element_domains.get(e, "unknown") for e in combination}
        if len(domains_in_combination) > 1:
            score += (len(domains_in_combination) - 1) * 2.0 # Bonus for each additional distinct domain

        # Normalize or scale if necessary, but for now, raw score is fine.
        return score

class PatentUniquenessChecker:
    """
    Performs a patent-style uniqueness check against a database of known solutions.
    A solution is considered unique if its canonical representation is not found
    in the known solutions database.
    """
    def __init__(self, known_solutions_db: Set[str]):
        self.known_solutions_db = known_solutions_db

    @staticmethod
    def _canonical_representation(combination: Tuple[str, ...]) -> str:
        """
        Generates a canonical string representation for a combination.
        This ensures that (A, B) and (B, A) are considered the same.
        We use a sorted, joined string, then hash it for efficient storage/comparison.
        """
        sorted_elements = tuple(sorted(combination))
        return hashlib.sha256("::".join(sorted_elements).encode('utf-8')).hexdigest()

    def is_unique(self, combination: Tuple[str, ...]) -> bool:
        """
        Checks if the given combination is unique against the known solutions database.
        """
        canonical_form = self._canonical_representation(combination)
        return canonical_form not in self.known_solutions_db

    def add_solution(self, combination: Tuple[str, ...]):
        """
        Adds a new unique solution to the database.
        """
        canonical_form = self._canonical_representation(combination)
        self.known_solutions_db.add(canonical_form)

class EmergentSynthesisEngine:
    """
    The core engine for generating novel solutions by combining existing patterns.
    It orchestrates combination generation, novelty scoring, and uniqueness checking.
    """
    def __init__(self, prior_art_elements: Set[str] = None, known_solutions_db: Set[str] = None, element_domains: Dict[str, str] = None):
        self.prior_art_elements = prior_art_elements if prior_art_elements is not None else set()
        self.known_solutions_db = known_solutions_db if known_solutions_db is not None else set()
        self.element_domains = element_domains if element_domains is not None else {}
        self.uniqueness_checker = PatentUniquenessChecker(self.known_solutions_db)

    def generate_combinations(self, patterns: List[str], min_elements: int = 2, max_elements: int = 3) -> List[Tuple[str, ...]]:
        """
        Generates all possible combinations of patterns within a specified range of element counts.
        """
        all_combinations = []
        for r in range(min_elements, max_elements + 1):
            all_combinations.extend(list(itertools.combinations(patterns, r)))
        return all_combinations

    def synthesize(self, patterns: List[str], min_elements: int = 2, max_elements: int = 3, top_n: int = 5) -> List[Dict[str, Any]]:
        """
        Executes the synthesis process:
        1. Generates combinations from provided patterns.
        2. Filters for uniqueness using the PatentUniquenessChecker.
        3. Scores novel unique combinations using the NoveltyScorer.
        4. Returns the top_n most novel combinations.
        """
        print(f"AIVA: Initiating Emergent Synthesis with {len(patterns)} patterns...")
        all_candidate_combinations = self.generate_combinations(patterns, min_elements, max_elements)
        print(f"AIVA: Generated {len(all_candidate_combinations)} raw combinations.")

        novel_candidates = []
        for combo in all_candidate_combinations:
            if self.uniqueness_checker.is_unique(combo):
                novelty_score = NoveltyScorer.score(combo, self.prior_art_elements, self.element_domains)
                novel_candidates.append({
                    "combination": combo,
                    "novelty_score": novelty_score,
                    "is_unique": True # Explicitly state it's unique
                })
                # Optionally, add to known_solutions_db once it's 'discovered' and considered valid
                # For this simulation, we'll only add it if it's selected as a top_n solution
            # else:
            #     print(f"AIVA: Combination {combo} is not unique (prior art exists).")

        print(f"AIVA: Identified {len(novel_candidates)} unique candidate combinations.")

        # Sort by novelty score in descending order
        novel_candidates.sort(key=lambda x: x["novelty_score"], reverse=True)

        # Select top_n and add them to the known solutions database
        final_solutions = []
        for solution in novel_candidates[:top_n]:
            self.uniqueness_checker.add_solution(solution["combination"])
            final_solutions.append(solution)
        
        print(f"AIVA: Selected {len(final_solutions)} top novel solutions.")
        return final_solutions

# Example Usage (for testing within the file)
if __name__ == "__main__":
    # AIVA's current knowledge base of patterns
    aiva_patterns = [
        "bio-luminescent algae", "self-repairing polymers", "neural network",
        "quantum entanglement", "atmospheric carbon capture", "modular robotics",
        "sonic levitation", "bio-mimetic sensors", "plasma confinement",
        "genetic algorithm", "nanoparticle delivery", "hydroponic nutrient cycling"
    ]

    # AIVA's prior art (elements commonly known or used)
    aiva_prior_art_elements = {
        "neural network", "genetic algorithm", "modular robotics", "bio-mimetic sensors"
    }

    # AIVA's current database of known solutions (canonical hashes)
    # This would typically be persistent across runs
    aiva_known_solutions = set()

    # AIVA's understanding of element domains for conceptual distance
    aiva_element_domains = {
        "bio-luminescent algae": "biology",
        "self-repairing polymers": "materials science",
        "neural network": "AI",
        "quantum entanglement": "physics",
        "atmospheric carbon capture": "environmental engineering",
        "modular robotics": "robotics",
        "sonic levitation": "physics",
        "bio-mimetic sensors": "biology",
        "plasma confinement": "physics",
        "genetic algorithm": "AI",
        "nanoparticle delivery": "biotechnology",
        "hydroponic nutrient cycling": "agriculture"
    }

    print("--- AIVA Flash 2.0 Agent Initializing Emergent Synthesis Engine ---")
    engine = EmergentSynthesisEngine(
        prior_art_elements=aiva_prior_art_elements,
        known_solutions_db=aiva_known_solutions,
        element_domains=aiva_element_domains
    )

    # First synthesis run
    print("\n--- First Synthesis Run ---")
    solutions_run1 = engine.synthesize(aiva_patterns, min_elements=2, max_elements=3, top_n=3)
    for i, sol in enumerate(solutions_run1):
        print(f"Solution {i+1}: {sol['combination']} (Novelty: {sol['novelty_score']:.2f})")

    # Add a manually 'discovered' solution to the known database to test uniqueness check
    print("\n--- Manually adding a known solution to simulate prior art ---")
    manual_solution = ("neural network", "modular robotics")
    engine.uniqueness_checker.add_solution(manual_solution)
    print(f"Added manual solution: {manual_solution}")

    # Second synthesis run with updated known solutions
    print("\n--- Second Synthesis Run (with updated known solutions) ---")
    solutions_run2 = engine.synthesize(aiva_patterns, min_elements=2, max_elements=3, top_n=3)
    for i, sol in enumerate(solutions_run2):
        print(f"Solution {i+1}: {sol['combination']} (Novelty: {sol['novelty_score']:.2f})")

    # Verify a known solution is no longer considered unique
    print("\n--- Verifying uniqueness check ---")
    test_combo_known = ("neural network", "modular robotics")
    test_combo_new = ("quantum entanglement", "hydroponic nutrient cycling")
    print(f"Is '{test_combo_known}' unique? {engine.uniqueness_checker.is_unique(test_combo_known)}")
    print(f"Is '{test_combo_new}' unique? {engine.uniqueness_checker.is_unique(test_combo_new)}")

    print("\n--- AIVA Flash 2.0 Agent: Synthesis Complete ---")
