
import os
import re
import threading
from datetime import datetime
from dotenv import load_dotenv

from .HAIUtils.HAIUtils import (
    getFrameworkInfo,
    formatJsonInput,
    formatJsonExtended,
    parseJsonInput,
    formatTypedInput,
    formatTypedExtended,
    parseTypedInput,
    mergeInstructions,
    isStructured,
    safetySettings,
    getFrames
)

from SkillLink import SkillLink as HoloLink
from SyncLink import SyncLink as HoloSync
from SynMem import SynMem as HoloMem
from SynLrn import SynLrn as HoloLrn
from BitSig import BitSig as HoloSig
load_dotenv()

PROVIDER_KEYS = {
    "openai": "OPENAI_API_KEY",
    "anthropic": "ANTHROPIC_API_KEY",
    "google": "GOOGLE_API_KEY",
    "groq": "GROQ_API_KEY",
}

def isKeySet(envKey):
    return os.getenv(envKey) is not None


MODEL_PREFIX_MAP = {
    ("gpt", "o1", "o3"): "openai",
    ("claude",): "anthropic",
    ("llama", "meta-llama", "gemma2"): "groq",
    ("gemini", "gemma",): "google",
}

providerMap = {}

if isKeySet("OPENAI_API_KEY"):
    from .HAIConfigs.OpenAIConfig import OpenAIConfig
    providerMap["openai"] = OpenAIConfig()

if isKeySet("ANTHROPIC_API_KEY"):
    from .HAIConfigs.AnthropicConfig import AnthropicConfig
    providerMap["anthropic"] = AnthropicConfig()

if isKeySet("GOOGLE_API_KEY"):
    from .HAIConfigs.GoogleConfig import GoogleConfig
    providerMap["google"] = GoogleConfig()

if isKeySet("GROQ_API_KEY"):
    from .HAIConfigs.GroqConfig import GroqConfig
    providerMap["groq"] = GroqConfig()



class HoloAI:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            with cls._lock:
                if not cls._instance:
                    cls._instance = super(HoloAI, cls).__new__(cls)
        return cls._instance

    def __init__(self):
        if getattr(self, 'initialized', False):
            return

        # Config objects
        #self.anthropic = AnthropicConfig()
        # self.openai    = OpenAIConfig()
        # self.google    = GoogleConfig()
        # self.groq      = GroqConfig()

        # self.providerMap = {
        #     #"anthropic": self.anthropic,
        #     "openai":    self.openai,
        #     "google":    self.google,
        #     "groq":      self.groq
        # }
        self.providerMap = providerMap

        self.initialized = True

    def getFrameworkInfo(self):
        """
        Returns a string with framework information.
        """
        return getFrameworkInfo()

    # def setProvider(self, provider: str):
    #     if provider not in self.providerMap:
    #         raise ValueError(f"Unknown provider '{provider}'. Valid providers: {list(self.providerMap)}")
    #     self.activeProvider = provider

    def listProviders(self):
        """
        Returns a list of available model providers.
        This is based on the keys of the providerMap dictionary.
        """
        return list(self.providerMap.keys())

    def _inferModelProvider(self, model: str):
        """
        Infers the provider based on the model name.
        Returns the provider name as a string, or None if not found.
        """
        return next(
            (provider for prefixes, provider in MODEL_PREFIX_MAP.items()
             if any(model.startswith(prefix) for prefix in prefixes)),
            None
        )

    def _getProviderConfig(self, model: str):
        """
        Returns the config instance strictly based on model's inferred provider.
        Raises if provider cannot be inferred.
        """
        provider = self._inferModelProvider(model)
        if provider and provider in self.providerMap:
            return self.providerMap[provider]
        raise ValueError(f"Cannot infer provider from model '{model}'. Valid providers: {list(self.providerMap.keys())}")

    def HoloCompletion(self, **kwargs):
        model = kwargs.get("model")
        raw   = kwargs.get("input") or kwargs.get("user")
        system = mergeInstructions(kwargs)
        verbose = kwargs.get("verbose", False)
        if not model or raw is None:
            raise ValueError("HoloCompletion needs both a model and input/user")

        # ————— 1) Isolate the *last* user message, not the whole history —————
        if isinstance(raw, list):
            last = raw[-1]
            text = last["content"] if isinstance(last, dict) and "content" in last else str(last)
        else:
            text = str(raw)

        # ————— 2) Find any image paths in that single prompt —————
        image_paths = self._extractImagePaths(text)
        #print(f"[DEBUG] Found image paths: {image_paths}")

        if image_paths:
            # strip off everything after the last path (so 'What is in this image? ' remains)
            img = image_paths[-1]
            prompt_only = re.split(re.escape(img), text)[0].strip()
            #print(f"[HoloCompletion] Vision → prompt='{prompt_only}', paths={image_paths}")
            return self.Vision(
                model=model,
                system=system, #kwargs.get("instructions") or kwargs.get("system"),
                user=prompt_only,
                paths=image_paths,
                collect=5,
                verbose=verbose
            )

        #("[HoloCompletion] Response")
        return self.Response(**kwargs)

    def Response(self, **kwargs):
        """
        Get a response from the configured model.
        :param kwargs: Keyword arguments to customize the request.
            - model: The model to use (optional).
            - system: System instructions (optional).
            - user: User input (optional).
            - verbose: Whether to return verbose output (default: False).
        :return: A response object.
        """
        #print(f"\n[Response Request] {kwargs}")
        model = kwargs.get('model')
        config = self._getProviderConfig(model)
        return config.getResponse(**kwargs)

    def Vision(self, **kwargs):
        """
        Get a vision response from the configured model.
        :param kwargs: Keyword arguments to customize the request.
            - model: The model to use (optional).
            - system: System instructions (optional).
            - user: User input (optional).
            - paths: List of image paths (default: empty list).
            - collect: Number of frames to collect (default: 5).
            - verbose: Whether to return verbose output (default: False).
        :return: A vision response object.
        """
        #print(f"\n[Vision Request] {kwargs}")
        model = kwargs.get('model')
        config = self._getProviderConfig(model)
        return config.getVision( **kwargs)

    #------------- Utility Methods -------------#
    def isStructured(self, obj):
        """
        Check if the input is a structured list of message dicts.
        A structured list is defined as a list of dictionaries where each dictionary
        contains both "role" and "content" keys.
        Returns True if the input is a structured list, False otherwise.
        """
        return isStructured(obj)

    def formatInput(self, value):
        """
        Formats the input value into a list.
        - If `value` is a string, returns a list containing that string.
        - If `value` is already a list, returns it as is.
        - If `value` is None, returns an empty list.
        """
        return [value] if isinstance(value, str) else value

    def formatConversation(self, convo, user):
        """
        Returns a flat list representing the full conversation:
        - If `convo` is a list, appends the user input (str or list) to it.
        - If `convo` is a string, creates a new list with convo and user input.
        """
        if isinstance(convo, str):
            convo = [convo]
        if isinstance(user, str):
            return convo + [user]
        elif isinstance(user, list):
            return convo + user
        else:
            raise TypeError("User input must be a string or list of strings.")


    def formatJsonInput(self, role: str, content: str) -> dict:
        """
        Format content for JSON-based APIs like OpenAI, Groq, etc.
        Converts role to lowercase and ensures it is one of the allowed roles.
        """
        return formatJsonInput(role=role, content=content)

    def formatJsonExtended(self, role: str, content: str) -> dict:
        """
        Extended JSON format for APIs like OpenAI, Groq, etc.
        Maps 'assistant', 'developer', 'model' and 'system' to 'assistant'.
        All other roles (including 'user') map to 'user'.
        """
        return formatJsonExtended(role=role, content=content)

    def parseJsonInput(self, data):
        """
        Accepts a string, a list of strings, or a list of message dicts/typed objects.
        Parses a single raw string with optional role prefix (user:, system:, developer:, assistant:)
        Returns a list of normalized message objects using formatJsonExtended.
        """
        return parseJsonInput(data)

    def formatTypedInput(self, role: str, content: str) -> dict:
        """
        Format content for typed APIs like Google GenAI.
        Converts role to lowercase and ensures it is one of the allowed roles.
        """
        return formatTypedInput(role=role, content=content)

    def formatTypedExtended(self, role: str, content: str) -> dict:
        """
        Extended typed format for Google GenAI APIs.
        Maps 'assistant', 'developer', 'system' and 'model' to 'model'.
        All other roles (including 'user') map to 'user'.
        """
        return formatTypedExtended(role=role, content=content)

    def parseTypedInput(self, data):
        """
        Accepts a string, a list of strings, or a list of message dicts/typed objects.
        Parses a single raw string with optional role prefix (user:, system:, developer:, assistant:)
        Returns a list of normalized Google GenAI message objects using formatTypedExtended.
        """
        return parseTypedInput(data)

    def safetySettings(self, **kwargs):
        """
        Construct a list of Google GenAI SafetySetting objects.

        Accepts thresholds as keyword arguments:
            harassment, hateSpeech, sexuallyExplicit, dangerousContent

        Example:
            safetySettings(harassment="block_high", hateSpeech="block_low")
        """
        return safetySettings(**kwargs)

    def _extractImagePaths(self, text: str):
        """
        Extracts image file paths from a given text.
        Supports both Windows and Unix-style paths.
        Returns a list of matched image paths.
        """
        win = r'([A-Za-z]:(?:\\|/).*?\.(?:png|jpe?g|gif|webp))'
        unix= r'(/[^ ]*?/.*?\.(?:png|jpe?g|gif|webp))'
        matches = re.findall(f"{win}|{unix}", text, re.IGNORECASE)
        # flatten tuple pairs
        return [p for pair in matches for p in pair if p]






# # Test prompts
# What is in this image? C:\Users\TechU\OneDrive\Pictures\Screenshot 2024-06-09 022121.png
# Compare these two images. Describe what is in each and highlight differences and similarities, C:\Users\TechU\OneDrive\Pictures\Screenshot 2024-06-09 022121.png, C:\Users\TechU\OneDrive\Pictures\Screenshot 2024-06-19 144830.png


# load_dotenv()  # Load environment variables from .env file if it exists

# OPENAI = "gpt-4.1"               # your OpenAI vision-capable model
# GROG   = "meta-llama/llama-4-scout-17b-16e-instruct"  # groq vision-capable model
# GOOGLE = "gemini-2.5-flash"      # google vision-capable model

# class OmniEngine:
#     def __init__(self):
#         self.client = HoloAI()
#         self.model = GROG
#         self.memories = []

#     def currentTime(self):
#         return datetime.now().strftime("%I:%M %p")

#     def currentDate(self):
#         return datetime.now().strftime("%B %d, %Y")

#     def addMemory(self, user, response, maxTurns=10):
#         self.memories.append(f"user:{user}")
#         self.memories.append(f"assistant:{response}")
#         if len(self.memories) > maxTurns * 2:
#             self.memories = self.memories[-maxTurns*2:]

#     def chat(self, user: str) -> str:
#         currentUser = "Tristan McBride Sr."
#         #system = f"The current user is {currentUser} and the current date and time is {self.currentDate()} {self.currentTime()}."
#         system = f"The current user is {currentUser}."
#         instructions = f"The current date and time is {self.currentDate()} {self.currentTime()}."
#         msgs = self.client.formatConversation(self.memories, user) # self.memories + self.client.formatInput(user)
#         #msgs = user
#         resp = self.client.HoloCompletion(
#             model=self.model,
#             system=system,
#             instructions=instructions,
#             input=msgs,
#             #verbose=True
#         )
#         if resp:
#             self.addMemory(user, resp)
#         return resp

#     def run(self):
#         while True:
#             prompt = input("You: ")
#             reply = self.chat(prompt)
#             print(f"\nOmni:\n{reply}\n")

# if __name__ == "__main__":
#     OmniEngine().run()