from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, List


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


# DType (Semantic Contract)
class DType(ABC):
    """
    Pure semantic type. Does NOT contain names.
    Names belong to Ports or CompositePart.
    """
    def __init__(self, metadata: Optional[Dict[str, Any]] = None):
        self.metadata = metadata or {}
        self.conditions: Optional[Dict[str, Any]] = None

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

    def set_conditions(self, conditions: Dict[str, Any]):
        self.conditions = conditions

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


# Composite Types
class CompositePart:
    """
    Represents one named component inside a CompositeType.
    Example:
        CompositePart("image", ImageType("nifti"))
    """
    def __init__(self, name: str, dtype: DType):
        self.name = name
        self.dtype = dtype

    def __repr__(self):
        return f"CompositePart(name={self.name}, dtype={self.dtype})"


class CompositeType(DType):
    """
    CompositeType is a LIST of CompositePart.
    Order matters. Names are preserved inside CompositePart.
    """
    def __init__(self, parts: List[CompositePart], metadata=None):
        super().__init__(metadata)
        self.parts = parts

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

        if len(self.parts) != len(other.parts):
            raise CompatibilityException("CompositeType length mismatch")

        for i in range(len(self.parts)):
            p1 = self.parts[i]
            p2 = other.parts[i]

            if p1.name != p2.name:
                raise CompatibilityException(
                    f"CompositeType part name mismatch: {p1.name} vs {p2.name}"
                )

            p1.dtype.can_connect_to(p2.dtype, conditions)

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


# MaskType:
class MaskType(DType):
    def __init__(self, metadata=None):
        super().__init__(metadata)

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


# 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)


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


# 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 Types:
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  # Set by Workflow builder

    def full_name(self):
        if self.parent_task:
            return f"{self.parent_task.name}.{self.name}"
        return self.name

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


# OutPort (fan-out supported):
class OutPort(Port):
    def __init__(self, name: str, dtype: DType):
        super().__init__(name, dtype)
        self._connections: List["InPort"] = []
        self.produced_asset = None  # Set by ExecutionContext

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

    def connect(self, in_port: "InPort", conditions: Optional[Dict[str, Any]] = None):
        # Fan-out allowed → no limit on connections
        in_port.can_connect_to(self, conditions)

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

    def disconnect(self, in_port: "InPort"):
        if in_port in self._connections:
            self._connections.remove(in_port)
            in_port._connected_output = None

    def get_downstream_ports(self):
        return list(self._connections)


# InPort:
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_connected(self):
        return self._connected_output is not None

    def is_ready(self, context):
        """
        True only if data exists in ExecutionContext.
        """
        if not self._connected_output:
            return False
        return context.has_data(self)

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

    def disconnect(self):
        if self._connected_output:
            self._connected_output._connections.remove(self)
            self._connected_output = None

    def get_upstream_port(self):
        return self._connected_output

    def validate(self):
        if self.required and not self.is_connected():
            raise CompatibilityException(f"InPort {self.full_name()} is required but not connected")
