"""Auto-generated UDTF for View: {{ view.external_id }}"""

from __future__ import annotations

import sys
import traceback
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Iterator

# NOTE: Helper functions are now defined inside eval() method for Unity Catalog compatibility
# This ensures they're available in Unity Catalog's execution context

from pyspark.sql.types import (
    StructType,
    StructField,
    StringType,
    LongType,
    DoubleType,
    BooleanType,
    TimestampType,
    ArrayType,
)

{% if use_udtf_decorator %}
from pyspark.sql.functions import udtf

@udtf
{% endif %}
class {{ view.external_id }}UDTF:
    """User-Defined Table Function for {{ view.external_id }} View.
    
    Generated from CDF View: {{ view.space }}.{{ view.external_id }} {{ view.version }}
    Uses scalar UDTF execution for Unity Catalog SQL registration compatibility.
    """

    def __init__(self) -> None:
        """Initialize UDTF (no parameters allowed when using analyze method).
        
        This method works for all three registration modes:
        - Unity Catalog UDTF (direct call): Parameters go to eval(), token initialized there
        - Unity Catalog View (via view): Parameters go to eval(), token initialized there
        - Session-scoped: Parameters go to eval(), token initialized there
        
        Token initialization happens in eval() for all modes to ensure compatibility
        with PySpark Connect requirements.
        """
        # Initialize instance variables
        self._token_cache: dict[str, str | float] | None = None
        self._init_error: str | None = None
        self._init_error_category: str | None = None
        self._init_success = True  # Mark as ready for eval() initialization

{% if include_analyze %}
    @staticmethod
    def analyze(
        client_id,
        client_secret,
        tenant_id,
        cdf_cluster,
        project,
{% for prop in properties %}
        {{ prop.name }}=None{% if not loop.last %},{% endif %}
{% endfor %}
    ):
        """Analyze method required by PySpark Connect for session-scoped UDTFs.
        
        This method is used by PySpark Connect to validate arguments and determine output schema.
        For Unity Catalog registration, this method is optional but harmless if present.
        
        Args:
            client_id: OAuth2 client ID column (required)
            client_secret: OAuth2 client secret column (required)
            tenant_id: Azure AD tenant ID column (required)
            cdf_cluster: CDF cluster URL column (required)
            project: CDF project name column (required)
{% for prop in properties %}
            {{ prop.name }}: {{ prop.description or prop.name }} column (optional, defaults to None)
{% endfor %}
        
        Returns:
            AnalyzeResult containing the output schema
        """
        # Lazy import to avoid circular import during serialization
        # Import from non-Connect module first to avoid circular import in pyspark.sql.connect.udtf
        schema = {{ view.external_id }}UDTF.outputSchema()
        # Use __import__ with fromlist to avoid triggering circular import
        try:
            # Try non-Connect module first (avoids circular import in connect.udtf)
            udtf_mod = __import__('pyspark.sql.udtf', fromlist=['AnalyzeResult'], level=0)
            AnalyzeResult = getattr(udtf_mod, 'AnalyzeResult')
            return AnalyzeResult(schema)
        except (ImportError, AttributeError):
            # Fallback: try to get from sys.modules if already loaded
            import sys
            if 'pyspark.sql.udtf' in sys.modules:
                mod = sys.modules['pyspark.sql.udtf']
                if hasattr(mod, 'AnalyzeResult'):
                    return mod.AnalyzeResult(schema)
            # Last resort: try connect module (may trigger circular import, but only when method is called)
            try:
                connect_mod = __import__('pyspark.sql.connect.udtf', fromlist=['AnalyzeResult'], level=0)
                AnalyzeResult = getattr(connect_mod, 'AnalyzeResult')
                return AnalyzeResult(schema)
            except (ImportError, AttributeError):
                raise ImportError("Could not import AnalyzeResult from pyspark.sql.udtf or pyspark.sql.connect.udtf")
{% endif %}

    def eval(
        self,
        client_id: str | None = None,
        client_secret: str | None = None,
        tenant_id: str | None = None,
        cdf_cluster: str | None = None,
        project: str | None = None,
{% for prop in properties %}
        {{ prop.name }}: object | None = None{% if not loop.last %},{% endif %}
{% endfor %}
    ) -> Iterator[tuple]:
        """Execute scalar UDTF and yield rows as tuples.
        
        This method works for all three registration modes:
        - Unity Catalog UDTF (direct call): All parameters passed here, token initialized on first call
        - Unity Catalog View (via view): All parameters passed here, token initialized on first call
        - Session-scoped: All parameters passed here, token initialized on first call
        
        Token initialization happens here for all modes to ensure compatibility with
        PySpark Connect requirements (__init__ must be parameter-free when analyze method exists).
        
        Args:
            client_id: OAuth2 client ID (scalar string)
            client_secret: OAuth2 client secret (scalar string)
            tenant_id: Azure AD tenant ID (scalar string)
            cdf_cluster: CDF cluster URL (scalar string)
            project: CDF project name (scalar string)
{% for prop in properties %}
            {{ prop.name }}: {{ prop.description or prop.name }} (scalar value)
{% endfor %}
        
        Yields:
            Tuples representing rows from the View
        """
        import sys
        
        # CRITICAL: Define helper functions inside eval() for Unity Catalog compatibility
        # Unity Catalog's execution context may not execute module-level code,
        # so all helper functions must be defined here to ensure they're available
        {% set skip_docstring = True %}
        {% filter indent(8, first=True, blank=True) %}
        {% include '_http_client.py.jinja' %}
        {% endfilter %}
        # Arrow conversion helpers removed for scalar-only execution.
        
node",
                    "limit": 1000,
                    "includeTyping": False,
                }
                if filter_json:
                    payload["filter"] = filter_json
                if next_cursor is not None:
                    payload["cursor"] = next_cursor

                response = _make_request(
                    method="POST",
                    url=full_url,
                    token=token,
                    json_data=payload,
                    timeout=30,
                )
                response_data = response.json()
                items = response_data.get("items", [])

                # Process items from this page
                for item in items:
                    row_values: list[object] = []

            raise

        return
    def _build_filter_json(self, filters: dict[str, object]) -> dict | None:
        """Build CDF API filter JSON from input parameters.
        
        Replicates SDK's filter serialization format exactly.
        
        Args:
            filters: Dictionary of filter conditions
            
        Returns:
            CDF API filter JSON dict or None if no filters
        """
        import json
        from datetime import datetime, timezone

        def _coerce_filter_value(value: object) -> object:
            """Normalize filter values to SDK-like shapes (supports JSON list strings and datetime objects)."""
            # Handle datetime objects (from SQL TIMESTAMP) - convert to ISO 8601
            if hasattr(value, 'timestamp') or hasattr(value, 'isoformat'):
                try:
                    dt = value
                    # Ensure it's timezone-aware and in UTC
                    if hasattr(dt, 'tzinfo'):
                        if dt.tzinfo is None:
                            dt = dt.replace(tzinfo=timezone.utc)
                        else:
                            dt = dt.astimezone(timezone.utc)
                    # Convert to ISO 8601 format (CDF API expects ISO 8601 for datetime properties)
                    if hasattr(dt, 'isoformat'):
                        return dt.isoformat(timespec='milliseconds').replace('+00:00', 'Z')
                    else:
                        return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
                except (AttributeError, TypeError):
                    pass
            
            # Also check for datetime-like objects with year/month/day attributes
            if hasattr(value, 'year') and hasattr(value, 'month') and hasattr(value, 'day'):
                try:
                    # Try to construct a datetime and convert
                    if hasattr(value, 'hour'):
                        dt = datetime(
                            value.year, value.month, value.day,
                            value.hour, value.minute, value.second,
                            getattr(value, 'microsecond', 0)
                        )
                    else:
                        dt = datetime(value.year, value.month, value.day)
                    # Assume UTC if no timezone info
                    if not hasattr(value, 'tzinfo') or value.tzinfo is None:
                        dt = dt.replace(tzinfo=timezone.utc)
                    return dt.isoformat(timespec='milliseconds').replace('+00:00', 'Z')
                except Exception:
                    pass
            
            # Handle string values (JSON list strings)
            if isinstance(value, str):
                stripped = value.strip()
                if stripped.startswith("[") and stripped.endswith("]"):
                    try:
                        parsed = json.loads(stripped)
                        if isinstance(parsed, list):
                            return parsed
                    except (json.JSONDecodeError, ValueError):
                        pass
            return value

        # Filter out None values and normalize list-like strings and datetime objects
        active_filters = {k: _coerce_filter_value(v) for k, v in filters.items() if v is not None}
        
        if not active_filters:
            return None
        
        # Build property references and filter conditions
        filter_conditions = []
        
        for prop_name, prop_value in active_filters.items():
            # Create property reference as list (matches SDK format)
            # Format: ["space", "view_external_id/view_version", "property_name"]
            property_ref = [
                "{{ view.space }}",
                "{{ view.external_id }}/{{ view.version }}",
                prop_name,
            ]
            
            # Handle different value types
            if isinstance(prop_value, list):
                # List values use In filter
                filter_conditions.append({
                    "in": {
                        "property": property_ref,
                        "values": prop_value,
                    }
                })
            else:
                # Single values use Equals filter
                filter_conditions.append({
                    "equals": {
                        "property": property_ref,
                        "value": prop_value,
                    }
                })
        
        # Combine multiple filters with And
        if len(filter_conditions) == 1:
            return filter_conditions[0]
        elif len(filter_conditions) > 1:
            return {"and": filter_conditions}
        else:
            return None

    @staticmethod
    def outputSchema() -> StructType:
        """Return the output schema for this UDTF."""
        fields = [
{% for prop in properties %}
            StructField("{{ prop.name }}", {{ prop.spark_type }}, nullable={{ prop.nullable }}),
{% endfor %}
            StructField("space", StringType(), nullable=False),
            StructField("external_id", StringType(), nullable=False),
            StructField("createdTime", TimestampType(), nullable=True),
            StructField("lastUpdatedTime", TimestampType(), nullable=True),
            StructField("deletedTime", TimestampType(), nullable=True),
        ]

        return StructType(fields)
