from __future__ import annotations

from typing import TYPE_CHECKING, Any
import uuid

from flownet.v1.compiler.errors import FlowDefinitionError
from flownet.v1.compiler.targets import (
    coerce_flow_program_ref_target,
    is_actor_target,
    is_stateless_target,
)

if TYPE_CHECKING:
    from flownet.v1.core.flow_program import FlowProgram


class Transformation:
    def __init__(
        self,
        flow_program: FlowProgram,
        target: Any,
        type: str = "map",
        *,
        operator_key: str | None = None,
        operator_config: dict[str, Any] | None = None,
    ):
        self.trans_id = uuid.uuid4().hex
        self.flow_program: FlowProgram = flow_program
        self.type = str(type)
        self.operator_key: str | None = operator_key
        self.operator_config: dict[str, Any] | None = operator_config
        self.target = target

        self.upstreams: list[Transformation] = []
        self.downstreams: list[Transformation] = []
        self.is_sink: bool = False
        self.is_return: bool = False
        self.pipe_flow: Any | None = None
        self.exception_handler_stack: list[Any | None] = []

        self._validate_target()
        self._validate_operator_key()
        self._validate_operator_config()

    def _validate_target(self) -> None:
        if is_actor_target(self.target) or is_stateless_target(self.target):
            return
        normalized_program_ref = coerce_flow_program_ref_target(self.target)
        if normalized_program_ref is not None:
            # Canonicalize legacy flow_uri/version style refs on compile boundary.
            self.target = normalized_program_ref
            return
        raise FlowDefinitionError(
            "Flow graph accepts actor/stateless targets or canonical flow-program refs.",
        )

    def _validate_operator_key(self) -> None:
        if self.operator_key is None:
            return
        if not isinstance(self.operator_key, str) or not self.operator_key.strip():
            raise ValueError("operator_key must be a non-empty string when provided.")

    def _validate_operator_config(self) -> None:
        if self.operator_config is None:
            return
        if not isinstance(self.operator_config, dict):
            raise ValueError("operator_config must be a dict when provided.")

    def add_upstream(self, upstream_trans: Transformation) -> None:
        self.upstreams.append(upstream_trans)
        upstream_trans.downstreams.append(self)


__all__ = [
    "Transformation",
]
