from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, List
# Codes Go Below:


# Errors:
class CompatibilityException(Exception):
    def __init__(self, reason: str):
        super().__init__(reason)
        self.reason = reason


# DType (Semantic Contract):
class DType(ABC):
    def __init__(self, metadata: Optional[Dict[str, Any]] = None):
        self.metadata = metadata or {}

    @abstractmethod
    def can_connect_to(self, other: "DType", conditions: Optional[Dict[str, Any]] = None):
        pass

    def __repr__(self):
        return f"{self.__class__.__name__}(metadata={self.metadata})"


# COMPOSITE TYPE:
class CompositeType(DType):
    """
    Represents a semantic bundle of multiple named DTypes.
    Example:
        CompositeType({
            "sheet1": TableType(...),
            "sheet2": TextType(...),
            "image": ImageType(...)
        })
    """
    def __init__(self, parts: Dict[str, DType], metadata=None):
        super().__init__(metadata)
        self.parts = parts  # Dict[str, DType]

    def can_connect_to(self, other: "DType", conditions=None):
        if not isinstance(other, CompositeType):
            raise CompatibilityException("Expected CompositeType")

        if self.parts.keys() != other.parts.keys():
            raise CompatibilityException("CompositeType Keys Mismatch")

        for key in self.parts:
            self.parts[key].can_connect_to(other.parts[key], conditions)

    def __repr__(self):
        return f"CompositeType({self.parts})"


# Image Types:
class ImageType(DType):
    def __init__(self, modality: Optional[str] = None, metadata=None):
        super().__init__(metadata)
        self.modality = modality

    def can_connect_to(self, other: DType, conditions=None):
        if not isinstance(other, ImageType):
            raise CompatibilityException("Target is not an ImageType")

        if self.modality and other.modality and self.modality != other.modality:
            raise CompatibilityException(
                f"Image modality mismatch: {self.modality} vs {other.modality}"
            )


class NIFTIImageType(ImageType):
    def __init__(self, metadata=None):
        super().__init__(modality="nifti", metadata=metadata)

    def can_connect_to(self, other: DType, conditions=None):
        super().can_connect_to(other, conditions)
        # NIFTI-specific rules here


class DICOMImageType(ImageType):
    def __init__(self, metadata=None):
        super().__init__(modality="dicom", metadata=metadata)

    def can_connect_to(self, other: DType, conditions=None):
        super().can_connect_to(other, conditions)
        # DICOM-specific rules here


# Table Types:
class TableType(DType):
    def __init__(self, columns: List[str], metadata=None):
        super().__init__(metadata)
        self.columns = columns

    def can_connect_to(self, other: DType, conditions=None):
        if not isinstance(other, TableType):
            raise CompatibilityException("Target is not a TableType")

        if self.columns != other.columns:
            raise CompatibilityException(
                f"Table schema mismatch: {self.columns} vs {other.columns}"
            )


class CSVTableType(TableType):
    def __init__(
        self,
        columns: List[str],
        delimiter: str = ",",
        encoding: str = "utf-8",
        metadata=None,
    ):
        super().__init__(columns, metadata)
        self.delimiter = delimiter
        self.encoding = encoding

    def can_connect_to(self, other: DType, conditions=None):
        if not isinstance(other, CSVTableType):
            raise CompatibilityException("CSV tables can only connect to CSV tables")

        super().can_connect_to(other, conditions)

        if self.delimiter != other.delimiter:
            raise CompatibilityException(
                f"CSV delimiter mismatch: {self.delimiter} vs {other.delimiter}"
            )


# Text Type:
class TextType(DType):
    def __init__(
        self,
        language: Optional[str] = None,
        encoding: Optional[str] = None,
        metadata=None,
    ):
        super().__init__(metadata)
        self.language = language
        self.encoding = encoding

    def can_connect_to(self, other: DType, conditions=None):
        if not isinstance(other, TextType):
            raise CompatibilityException("Target is not TextType")

        if self.language and other.language and self.language != other.language:
            raise CompatibilityException(
                f"Language mismatch: {self.language} vs {other.language}"
            )

        if self.encoding and other.encoding and self.encoding != other.encoding:
            raise CompatibilityException(
                f"Encoding mismatch: {self.encoding} vs {other.encoding}"
            )


# Ports:
class Port(ABC):
    def __init__(self, name: str, dtype: DType):
        self.name = name
        self.dtype = dtype
        self.parent_task = None

    def __repr__(self):
        return f"{self.__class__.__name__}({self.name}, {self.dtype})"


class OutPort(Port):
    def __init__(self, name: str, dtype: DType):
        super().__init__(name, dtype)
        self._connections: List["InPort"] = []

    @property
    def connections(self):
        return list(self._connections)

    def connect(self, in_port: "InPort", conditions: Optional[Dict[str, Any]] = None):
        if len(self._connections):
            raise CompatibilityException("OutPort max connections reached")

        in_port.can_connect_to(self, conditions)

        self._connections.append(in_port)
        in_port._connected_output = self


class InPort(Port):
    def __init__(self, name: str, dtype: DType, required: bool = True):
        super().__init__(name, dtype)
        self.required = required
        self._connected_output: Optional[OutPort] = None

    @property
    def connected_output(self):
        return self._connected_output

    def is_ready(self):
        return self._connected_output is not None

    def can_connect_to(self, out_port: OutPort, conditions: Optional[Dict[str, Any]] = None):
        self.dtype.can_connect_to(out_port.dtype, conditions)
