"""
Shim layer for backward compatibility and v0 utility functions.

WARNING: This is an unstable API subject to change.

The executor classes now live in backends/legacy_executor.py (LegacyExecutor).
This module re-exports them for backward compatibility and keeps
shim-layer utilities that have no natural home in backends.
"""

from __future__ import annotations

from ..semantics.backends.legacy_executor import (
    LegacyExecutor,
    EXPORT_TO_CSV_RESERVED_TABLE_NAME,
)

try:
    from v0.relationalai.clients.snowflake import Provider as v0Provider  # type: ignore
except ImportError:
    from v0.relationalai.clients.resources.snowflake import Provider as v0Provider

from v0.relationalai.clients.config import Config as V0Config

from ..config import Config, create_config
from ..config.v1_to_v0_translator import translate_v1_to_v0
from ..semantics import Fragment
from ..semantics.frontend.base import Table
from ..semantics.metamodel import metamodel as mm
from ..semantics.frontend.pprint import format as mm_format

from ..util.error import warn

def get_provider(config: Config | None = None):
    """
    Get the v0 Snowflake provider instance with translated config.

    Parameters
    ----------
    config : Config | None
        The v1 configuration. If None, creates a default Config.

    Returns
    -------
    v0Provider
        The v0 Snowflake provider for managing Snowflake resources.
    """
    resolved_config = config if config is not None else create_config()
    v0_config = V0Config(translate_v1_to_v0(resolved_config), fetch=False)
    return v0Provider(config=v0_config)


def exec_batch(fragments: list[Fragment]):
    """
    Execute multiple export fragments as a single batch transaction.
    All fragments are submitted together — the backend may execute them in parallel.

    Parameters
    ----------
    fragments : list[Fragment]
        A list of PyRel export fragments to batch-execute. Each fragment must
        have a destination table set via .into(). All fragments must belong to
        the same model.

    Raises
    ------
    ValueError
        If any fragment is missing an .into() destination, if fragments
        belong to different models, or if fragments have mixed update/non-update
        settings (all must agree on the update flag).

    Examples
    --------
    Single fragment:

    >>> exec_batch([select(Person.id, Person.name).into(person_table)])

    Multiple fragments (may execute in parallel on the backend):

    >>> m = Model("SalesData")
    >>> Customer = m.Concept("Customer")
    >>> Product = m.Concept("Product")
    >>> exec_batch([
    ...     select(Customer.id, Customer.name).into(customer_table),
    ...     select(Product.id, Product.name).into(product_table),
    ... ])
    """
    if not fragments:
        warn("no-exports", "No fragments provided! Skipping execution.")
        return

    for i, frag in enumerate(fragments):
        if frag._into is None:
            raise ValueError(f"Fragment at index {i} must have a destination table set via .into().")

    if len({id(f._model) for f in fragments}) > 1:
        raise ValueError("All fragments must belong to the same model.")

    update_flags = {f._is_into_update for f in fragments}
    if len(update_flags) > 1:
        raise ValueError(
            "All fragments must have the same update setting. "
            "Mixed update/non-update exports cannot be batched in a single call."
        )
    update = update_flags.pop()

    for i, frag in enumerate(fragments):
        if frag._has_executed:
            warn("already-executed", f"Fragment at index {i} has already been executed! Skipping it.")

    pending = [f for f in fragments if not f._has_executed]
    if not pending:
        warn("no-exports", "All fragments have already been executed! Skipping execution.")
        return

    model = pending[0]._model
    mm_model = model.to_metamodel()

    export_tasks = []
    dsl_parts = []
    for frag in pending:
        export_tasks.append(frag.to_metamodel())
        dsl_parts.append(format(frag))

    mm_query = mm.Logical(body=tuple(export_tasks))
    executor = model._get_executor()
    assert isinstance(executor, LegacyExecutor)

    meta = {"operation": "exec", "num_exports": len(fragments), "exports_meta": [fr._meta for fr in fragments]}

    executor.execute_mm(
        "\n\n".join(dsl_parts),
        mm_query,
        mm_model,
        "lqp",
        "",
        update,
        meta,
    )

    for frag in pending:
        frag._has_executed = True


def export_to_csv(fragments: list[Fragment]) -> list[str]:
    """
    Export one or more query fragments to CSV in Snowflake stage area.
    All fragments are submitted as a single batch transaction — the backend
    may execute them in parallel.

    Parameters
    ----------
    fragments : list[Fragment]
        A list of PyRel query fragments to batch-export. Each fragment must
        contain a SELECT statement. All fragments must belong to the same model.

    Returns
    -------
    list[str]
        A list with one entry per input fragment (in the same order) containing the exported CSV
        file path.

    Raises
    ------
    ValueError
        If any fragment is missing a SELECT statement, or if fragments belong
        to different models.
    NotImplementedError
        If any fragment is an update export.

    Examples
    --------
    Single fragment:

    >>> [csv_path] = export_to_csv([select(Person.id, Person.name)])

    Multiple fragments (may execute in parallel on the backend):

    >>> m = Model("SalesData")
    >>> Customer = m.Concept("Customer")
    >>> Product = m.Concept("Product")
    >>> csv_paths = export_to_csv([select(Customer.id, Customer.name),
    ...                            select(Product.id, Product.name)])
    """
    if not fragments:
        warn("no-exports", "No fragments provided! Skipping execution.")
        return []

    for i, frag in enumerate(fragments):
        if not frag._select:
            raise ValueError(f"Fragment at index {i} must contain a SELECT statement.")

    if any(f._is_into_update for f in fragments):
        raise NotImplementedError("Batch execution of update exports is not currently supported.")

    if len({id(f._model) for f in fragments}) > 1:
        raise ValueError("All fragments must belong to the same model.")

    model = fragments[0]._model
    mm_model = model.to_metamodel()

    export_tasks = []
    dsl_parts = []
    for i, frag in enumerate(fragments):
        f = Fragment(model=model, parent=frag)
        f._into = Table(f"{EXPORT_TO_CSV_RESERVED_TABLE_NAME}_{i}", schema={}, model=model)
        f._is_into_update = False
        export_tasks.append(f.to_metamodel())
        dsl_parts.append(mm_format(f))

    mm_query = mm.Logical(body=tuple(export_tasks))
    executor = model._get_executor()
    assert isinstance(executor, LegacyExecutor)

    meta = {"operation": "export_to_csv", "num_exports": len(fragments), "exports_meta": [fr._meta for fr in fragments]}

    result = executor.execute_mm(
        "\n\n".join(dsl_parts),
        mm_query,
        mm_model,
        "lqp",
        EXPORT_TO_CSV_RESERVED_TABLE_NAME,
        False,
        meta,
    )

    # The backend returns one path row per export task in the same order as the input
    # fragments (task index 0, 1, ..., N-1), so zip(keys, paths) in the caller is safe.
    if result.empty:
        raise RuntimeError("export_to_csv returned empty result.")
    if result.columns.to_list() != ["path"]:
        raise RuntimeError(f"Expected only 'path' column in export result, but got: {result.columns.to_list()}")
    if len(result) != len(fragments):
        raise RuntimeError(f"Expected {len(fragments)} rows in export result, but got {len(result)}.")
    final_result = result["path"].tolist()
    if not all(isinstance(x, str) for x in final_result):
        raise RuntimeError("Expected all 'path' values to be strings.")

    return final_result
