from __future__ import annotations

from collections.abc import Callable, Mapping
from typing import Any

from flownet.compiler.flow_context import flow_build_context
from flownet.compiler.flowtask import FlowTask
from flownet.core.exceptions import FlowDefinitionError


def compile_v0_flow_task(
    flowfunction: Callable[..., Any],
    *,
    instance: Any | None,
) -> FlowTask:
    from flownet.v1.compiler.errors import FlowDefinitionError as V1FlowDefinitionError

    try:
        v1_program = _compile_with_v1_compiler(flowfunction, instance=instance)
        task = _convert_v1_program_to_legacy_task(v1_program)
    except V1FlowDefinitionError:
        task = _compile_with_v0_stack(flowfunction, instance=instance)
    except FlowDefinitionError:
        task = _compile_with_v0_stack(flowfunction, instance=instance)
    _validate_pipe_subflows(task)
    return task


def _compile_with_v1_compiler(
    flowfunction: Callable[..., Any],
    *,
    instance: Any | None,
):
    from flownet.v1.compiler import compile_flow_program

    def _wrapped_flow(init_stream):
        from flownet.api.flow_exception_handlers import (
            _clear_current_flowtask,
            _set_current_flowtask,
        )

        with flow_build_context():
            _set_current_flowtask(getattr(init_stream, "flow_program", None))
            try:
                if instance is None:
                    return flowfunction(init_stream)
                return flowfunction(instance, init_stream)
            finally:
                _clear_current_flowtask()

    return compile_flow_program(_wrapped_flow)


def _compile_with_v0_stack(
    flowfunction: Callable[..., Any],
    *,
    instance: Any | None,
) -> FlowTask:
    from flownet.api.connected_streams import ConnectedStreams
    from flownet.api.datastream import DataStream, SinkStream
    from flownet.api.flow_exception_handlers import _clear_current_flowtask, _set_current_flowtask

    task = FlowTask()
    init_stream = DataStream(task, transformation=None, init=True)

    with flow_build_context():
        _set_current_flowtask(task)
        try:
            if instance is None:
                output = flowfunction(init_stream)
            else:
                output = flowfunction(instance, init_stream)
        finally:
            _clear_current_flowtask()

    if output is None:
        task.no_return = True
    elif isinstance(output, SinkStream):
        task.no_return = True
    elif isinstance(output, ConnectedStreams):
        output.set_return()
    elif isinstance(output, DataStream):
        output.set_return()
    else:
        raise FlowDefinitionError(
            "Flow must return DataStream/ConnectedStreams/SinkStream/None, "
            f"got {type(output)}"
        )

    return task


def _convert_v1_program_to_legacy_task(v1_program: Any) -> FlowTask:
    from flownet.compiler.transformation import Transformation

    raw_pipeline = getattr(v1_program, "pipeline", None)
    if not isinstance(raw_pipeline, list):
        raise FlowDefinitionError("v1 compile facade produced invalid pipeline metadata.")

    task = FlowTask()
    transformed_by_id: dict[int, Transformation] = {}

    for raw_transformation in raw_pipeline:
        target = _coerce_legacy_target_required(getattr(raw_transformation, "target", None))
        legacy_transformation = Transformation(
            task,
            target,
            type=_normalize_operator_type(getattr(raw_transformation, "type", None)),
            operator_key=getattr(raw_transformation, "operator_key", None),
            operator_config=None,
        )
        raw_trans_id = getattr(raw_transformation, "trans_id", None)
        if isinstance(raw_trans_id, str) and raw_trans_id:
            legacy_transformation.trans_id = raw_trans_id
        legacy_transformation.is_sink = bool(getattr(raw_transformation, "is_sink", False))
        legacy_transformation.is_return = bool(getattr(raw_transformation, "is_return", False))
        legacy_transformation.pipe_flow = getattr(raw_transformation, "pipe_flow", None)
        raw_stack = getattr(raw_transformation, "exception_handler_stack", None)
        if isinstance(raw_stack, list):
            legacy_transformation.exception_handler_stack = [
                _coerce_legacy_handler(handler)
                for handler in raw_stack
            ]
        transformed_by_id[id(raw_transformation)] = legacy_transformation
        task.pipeline.append(legacy_transformation)

    for raw_transformation in raw_pipeline:
        legacy_transformation = transformed_by_id[id(raw_transformation)]
        legacy_transformation.operator_config = _coerce_config_value(
            getattr(raw_transformation, "operator_config", None),
            transformed_by_id=transformed_by_id,
        )
        upstreams = getattr(raw_transformation, "upstreams", None)
        if isinstance(upstreams, list):
            for upstream in upstreams:
                mapped_upstream = transformed_by_id.get(id(upstream))
                if mapped_upstream is None:
                    continue
                legacy_transformation.add_upstream(mapped_upstream)

    task.transformations = {trans.trans_id: trans for trans in task.pipeline}
    task.entry_id = _coerce_optional_str(getattr(v1_program, "entry_id", None))
    task.entry_ids = _coerce_id_list(getattr(v1_program, "entry_ids", None))
    if task.entry_id is not None and task.entry_id not in task.entry_ids:
        task.entry_ids.insert(0, task.entry_id)
    task.return_ids = _coerce_id_list(getattr(v1_program, "return_ids", None))
    task.sink_ids = _coerce_id_list(getattr(v1_program, "sink_ids", None))
    task.no_return = bool(getattr(v1_program, "no_return", True))
    program_stack = getattr(v1_program, "_exception_handler_stack", None)
    if isinstance(program_stack, list):
        task._exception_handler_stack = [
            _coerce_legacy_handler(handler)
            for handler in program_stack
        ]

    for attr_name in ("flow_uri", "scheduler", "resources", "metadata", "dsl_name"):
        if hasattr(v1_program, attr_name):
            setattr(task, attr_name, _clone_value(getattr(v1_program, attr_name)))

    return task


def _normalize_operator_type(operator_type: Any) -> str:
    normalized = str(operator_type or "").strip()
    if normalized == "process":
        return "pipe"
    if normalized:
        return normalized
    return "map"


def _coerce_legacy_target_required(target: Any):
    coerced = _try_coerce_legacy_target(target)
    if coerced is not None:
        return coerced
    raise FlowDefinitionError(
        "v1 compile facade produced unsupported target for v0 runtime: "
        f"{type(target)}"
    )


def _coerce_legacy_handler(handler: Any):
    if handler is None:
        return None
    coerced = _try_coerce_legacy_target(handler)
    if coerced is None:
        raise FlowDefinitionError(
            "v1 compile facade produced unsupported exception handler target: "
            f"{type(handler)}"
        )
    from flownet.api.actor_method_ref import ActorMethodRef

    if not isinstance(coerced, ActorMethodRef):
        raise FlowDefinitionError(
            "exception handler must resolve to ActorMethodRef, got "
            f"{type(coerced)}"
        )
    return coerced


def _coerce_config_value(value: Any, *, transformed_by_id: Mapping[int, Any]) -> Any:
    mapped_transformation = transformed_by_id.get(id(value))
    if mapped_transformation is not None:
        return mapped_transformation
    if isinstance(value, Mapping):
        return {
            key: _coerce_config_value(item, transformed_by_id=transformed_by_id)
            for key, item in value.items()
        }
    if isinstance(value, list):
        return [
            _coerce_config_value(item, transformed_by_id=transformed_by_id)
            for item in value
        ]
    if isinstance(value, tuple):
        return tuple(
            _coerce_config_value(item, transformed_by_id=transformed_by_id)
            for item in value
        )
    coerced = _try_coerce_legacy_target(value)
    if coerced is not None:
        return coerced
    return value


def _try_coerce_legacy_target(value: Any):
    from flownet.api.actor_method_ref import ActorMethodRef
    from flownet.core.flow_program import FlowProgramRef
    from flownet.core.stateless import StatelessOpRef

    if isinstance(value, (ActorMethodRef, StatelessOpRef, FlowProgramRef)):
        return value

    program_uri = _coerce_optional_str(_field(value, "program_uri"))
    program_rev = _coerce_optional_str(_field(value, "program_rev"))
    if program_uri is not None and program_rev is not None:
        # v1 canonical refs lose the original object identity used by v0 tests
        # and runtime contracts, so this case intentionally falls back to v0 compile.
        return None

    flow_uri = _coerce_optional_str(_field(value, "flow_uri"))
    version = _coerce_optional_str(_field(value, "version"))
    if flow_uri is not None and version is not None:
        return FlowProgramRef(flow_uri=flow_uri, version=version)

    address = _coerce_optional_str(_field(value, "address"))
    actor_id = _coerce_optional_str(_field(value, "actor_id"))
    method = _coerce_optional_str(_field(value, "method"))
    if address is not None and actor_id is not None and method is not None:
        return ActorMethodRef(
            address=address,
            actor_id=actor_id,
            method=method,
        )

    op_id = _coerce_optional_str(_field(value, "op_id"))
    scope = _coerce_optional_str(_field(value, "scope"))
    if op_id is not None and scope is not None:
        return StatelessOpRef(
            op_id=op_id,
            scope=scope,  # type: ignore[arg-type]
            name=_coerce_optional_str(_field(value, "name")),
            module=_coerce_optional_str(_field(value, "module")),
            qualname=_coerce_optional_str(_field(value, "qualname")),
            origin=_coerce_optional_str(_field(value, "origin")),
            config=_field(value, "config"),
        )
    return None


def _field(value: Any, name: str) -> Any:
    if isinstance(value, Mapping):
        return value.get(name)
    return getattr(value, name, None)


def _coerce_optional_str(value: Any) -> str | None:
    if not isinstance(value, str):
        return None
    normalized = value.strip()
    if not normalized:
        return None
    return normalized


def _coerce_id_list(value: Any) -> list[str]:
    if not isinstance(value, list):
        return []
    result: list[str] = []
    for item in value:
        normalized = _coerce_optional_str(item)
        if normalized is not None:
            result.append(normalized)
    return result


def _clone_value(value: Any) -> Any:
    if isinstance(value, Mapping):
        return {key: _clone_value(item) for key, item in value.items()}
    if isinstance(value, list):
        return [_clone_value(item) for item in value]
    if isinstance(value, tuple):
        return tuple(_clone_value(item) for item in value)
    return value


def _validate_pipe_subflows(task: FlowTask) -> None:
    for transformation in task.pipeline:
        if transformation.type != "pipe" or transformation.pipe_flow is None:
            continue
        subflow = transformation.pipe_flow
        try:
            subtask = subflow.compile()
        except Exception as exc:
            raise FlowDefinitionError(
                "Failed to compile pipe() subflow "
                f"(transformation_id={transformation.trans_id}, "
                f"subflow={type(subflow).__name__})."
            ) from exc
        if subtask.no_return and transformation.downstreams:
            raise FlowDefinitionError(
                "pipe() subflow has no return but downstream operators exist."
            )


__all__ = [
    "compile_v0_flow_task",
]
