import ast
from typing import Any

from ._exceptions import (
   InvalidArgumentError, 
   MissingLabelError,
   SYMBOL,
) 
from ._var_analysis import VALUE_TYPE_ERROR


INVALID_LABEL = "Label cannot be empty or blank: `{label}`"
LABEL_TYPE_ERROR = "Label must be a string."


def _check_exist_label(self, label: str) -> None:
    try:
        self._new_value[label]
    except KeyError:
        raise MissingLabelError(label)
    
def _try_search_var(
      self, node: ast.Name | ast.Attribute, 
      err_check=False, get_full_name: bool=False,
) -> tuple[str, Any] | str:
   if isinstance(node, ast.Name):
      try:
         ret_value = self._frame.f_locals[node.id]
      except KeyError:
         ret_value = self._frame.f_globals[node.id]

      var_name = node.id 

      if get_full_name:
         return (var_name, var_name)  
   else:
      try:
         ret_value = self._frame.f_locals[node.value.id]
         var_name = node.value.id
      except KeyError:
         ret_value = self._frame.f_globals[node.value.id]
         var_name = node.value.id
      except AttributeError:
         if err_check:
            raise InvalidArgumentError(VALUE_TYPE_ERROR)
         
         (attr_id, attr_full_name) = _get_attr_fullname(node)

         try:
            ret_value = self._frame.f_locals[attr_id]
         except KeyError:
            ret_value = self._frame.f_globals[attr_id]
            
         var_name = attr_id

         if get_full_name:
            return (var_name, attr_full_name)
         
      if get_full_name:
         return (var_name, f"{var_name}.{node.attr}")

      if err_check:
         return var_name
      
   return (var_name, ret_value)
   

def _get_attr_fullname(node: ast.Attribute) -> tuple[str, str]:
   name = []

   while isinstance(node, ast.Attribute):
      name.append(node.attr)
      node = node.value

   if isinstance(node, ast.Name):
      name.append(node.id)
      name.reverse()
   else:
      raise TypeError(
         "The attribute structure used in `cond` is not supported."
      )

   return (name[0], ".".join(name))

def _get_nested_value(self, attr_name: str) -> str | int:
   parts = attr_name.split(".")

   try:
      ref = self._frame.f_locals[parts[0]]
   except KeyError:
      try:
         ref = self._frame.f_globals[parts[0]]
      except KeyError:
         raise InvalidArgumentError(
            f"`{attr_name}` is not defined in the current scope."
         )
      
   for attr in parts[1:]:
      ref = getattr(ref, attr)

   return ref

def _compare_value_counts(
      self, label: str, needed_indices_count: int,
) -> None:
   # Note: `len(total_values)` is always one more than the max index.

   if not isinstance(needed_indices_count, int):
      raise InvalidArgumentError("`index` must be a integer.")

   if len(self._new_value[label]) < needed_indices_count + 1:
      raise ValueError(
         f"Please set the new value for index {needed_indices_count}."
      )
   
def _count_symbol(self, label: str) -> int:
   if not isinstance(label, str):
      raise InvalidArgumentError(LABEL_TYPE_ERROR)
   
   count = 0

   # count only prefix symbols
   for symbol in label:
      if symbol == SYMBOL:
         count += 1
         continue
      return count
    

def _check_label_type(label: Any, allow_dict: bool=True) -> None:
   if not allow_dict and isinstance(label, dict):
      raise InvalidArgumentError(
         "`dict` type is not allowed for `label` in this function."
      )
   elif isinstance(label, (list, tuple)):
      return
   elif not isinstance(label, str):
      raise InvalidArgumentError(LABEL_TYPE_ERROR)
   elif label == "" or " " in label:
      raise InvalidArgumentError(INVALID_LABEL.format(label=label))


def _handle_arg_types(
      label: Any, value: Any | None, index: int=None,
) -> dict[str, Any]:
   # check `index` type
   if index is not None and not isinstance(index, int):
      raise InvalidArgumentError(
         "The `index` keyword argument must be an `int`."
      )
      
   # single label
   if isinstance(label, str):    
      if label == "" or " " in label:
         raise InvalidArgumentError(INVALID_LABEL.format(label=key))  
      elif isinstance(value, (list, tuple)):          
        return {label: value}    
      else:        
         return {label: value}   
   elif isinstance(label, dict):
      # multiple labels
      if len(label) == 0:
         raise InvalidArgumentError(
            "Please provide both a key and a value for the dictionary."
         )   
      elif value is None:
         for key in label.keys():
            if not isinstance(key, str):
               raise InvalidArgumentError(LABEL_TYPE_ERROR)
            elif key == "" or " " in key:
               raise InvalidArgumentError(INVALID_LABEL.format(label=key))
         
         return label
      
      raise InvalidArgumentError(
         "All required values must be included in the dictionary provided to `label` "
         "Do not pass them as separate arguments."
      )
   
   else:
      raise InvalidArgumentError("`label` must be a string or a dictionary.")