"""
koci Expression Evaluator

Evaluates conditional expressions for job/step execution:
- Simple 'when' conditions: branch == 'main', tag =~ 'v*'
- Complex 'if' expressions: ${{ success() && env.DEPLOY == 'true' }}
"""

import re
import operator
from typing import Any, Optional
from dataclasses import dataclass

from .exc import KociError


class ExpressionError(KociError):
    """Error evaluating an expression."""
    pass


@dataclass
class ExecutionContext:
    """
    Context available during expression evaluation.

    Provides access to:
    - Branch/tag information
    - Environment variables
    - Job status information
    - Custom variables
    """
    branch: Optional[str] = None
    tag: Optional[str] = None
    event: str = "push"  # push, pull_request, schedule, manual
    env: dict[str, str] = None
    variables: dict[str, Any] = None
    job_results: dict[str, str] = None  # job_name -> success/failure/skipped

    def __post_init__(self):
        if self.env is None:
            self.env = {}
        if self.variables is None:
            self.variables = {}
        if self.job_results is None:
            self.job_results = {}


class SimpleExpressionEvaluator:
    """
    Evaluates simple 'when' conditions.

    Supported syntax:
        branch == 'main'
        branch != 'develop'
        branch =~ 'feature/*'
        branch !~ 'release/*'
        tag == 'v1.0.0'
        tag =~ 'v*'
        event == 'push'
        env.DEBUG == 'true'
        always
        never

    Compound expressions:
        branch == 'main' && tag =~ 'v*'
        branch == 'develop' || branch == 'main'
    """

    OPERATORS = {
        '==': operator.eq,
        '!=': operator.ne,
        '=~': lambda a, b: bool(re.match(b.replace('*', '.*'), a or '')),
        '!~': lambda a, b: not bool(re.match(b.replace('*', '.*'), a or '')),
    }

    def __init__(self, context: ExecutionContext):
        self.context = context

    def evaluate(self, expression: str) -> bool:
        """
        Evaluate a simple expression.

        Args:
            expression: The when condition string

        Returns:
            Boolean result of evaluation
        """
        if not expression:
            return True

        expression = expression.strip()

        # Handle special keywords
        if expression.lower() == 'always':
            return True
        if expression.lower() == 'never':
            return False

        # Handle compound expressions
        if ' && ' in expression:
            parts = expression.split(' && ')
            return all(self.evaluate(p.strip()) for p in parts)

        if ' || ' in expression:
            parts = expression.split(' || ')
            return any(self.evaluate(p.strip()) for p in parts)

        # Parse single condition
        return self._evaluate_condition(expression)

    def _evaluate_condition(self, condition: str) -> bool:
        """Evaluate a single condition."""
        # Match: variable operator 'value' or variable operator "value"
        match = re.match(
            r"(\w+(?:\.\w+)?)\s*(==|!=|=~|!~)\s*['\"](.+?)['\"]",
            condition
        )

        if not match:
            raise ExpressionError(f"Invalid condition syntax: {condition}")

        var_path, op, value = match.groups()

        # Get variable value
        var_value = self._get_variable(var_path)

        # Apply operator
        if op not in self.OPERATORS:
            raise ExpressionError(f"Unknown operator: {op}")

        try:
            return self.OPERATORS[op](var_value, value)
        except Exception as e:
            raise ExpressionError(f"Error evaluating {condition}: {e}")

    def _get_variable(self, path: str) -> Any:
        """Get a variable value from context."""
        parts = path.split('.')

        if parts[0] == 'branch':
            return self.context.branch
        elif parts[0] == 'tag':
            return self.context.tag
        elif parts[0] == 'event':
            return self.context.event
        elif parts[0] == 'env':
            if len(parts) > 1:
                return self.context.env.get(parts[1], '')
            return ''
        elif parts[0] == 'var' or parts[0] == 'variables':
            if len(parts) > 1:
                return self.context.variables.get(parts[1], '')
            return ''
        else:
            # Try direct lookup in variables
            return self.context.variables.get(path, '')


class ComplexExpressionEvaluator:
    """
    Evaluates complex 'if' expressions with ${{ }} syntax.

    Supported syntax:
        ${{ success() }}
        ${{ failure() }}
        ${{ always() }}
        ${{ cancelled() }}
        ${{ env.DEBUG == 'true' }}
        ${{ needs.build.result == 'success' }}
        ${{ github.event_name == 'push' }}
        ${{ success() && env.DEPLOY == 'true' }}
        ${{ !failure() }}

    Functions:
        success() - True if all previous jobs succeeded
        failure() - True if any previous job failed
        always() - Always true (run regardless of status)
        cancelled() - True if workflow was cancelled
    """

    def __init__(self, context: ExecutionContext):
        self.context = context

    def evaluate(self, expression: str) -> bool:
        """
        Evaluate a complex if expression.

        Args:
            expression: The if condition string (may include ${{ }})

        Returns:
            Boolean result of evaluation
        """
        if not expression:
            return True

        expression = expression.strip()

        # Extract expression from ${{ }}
        match = re.match(r'\$\{\{\s*(.+?)\s*\}\}', expression)
        if match:
            expression = match.group(1)

        # Evaluate the expression
        return self._evaluate(expression)

    def _evaluate(self, expr: str) -> bool:
        """Recursively evaluate expression."""
        expr = expr.strip()

        # Handle negation
        if expr.startswith('!'):
            return not self._evaluate(expr[1:].strip())

        # Handle parentheses
        if expr.startswith('(') and expr.endswith(')'):
            return self._evaluate(expr[1:-1])

        # Handle && (and)
        and_parts = self._split_logical(expr, '&&')
        if len(and_parts) > 1:
            return all(self._evaluate(p) for p in and_parts)

        # Handle || (or)
        or_parts = self._split_logical(expr, '||')
        if len(or_parts) > 1:
            return any(self._evaluate(p) for p in or_parts)

        # Handle function calls
        func_match = re.match(r'(\w+)\(\)', expr)
        if func_match:
            return self._call_function(func_match.group(1))

        # Handle comparisons
        return self._evaluate_comparison(expr)

    def _split_logical(self, expr: str, operator: str) -> list[str]:
        """Split expression by logical operator, respecting parentheses."""
        parts = []
        current = []
        depth = 0
        i = 0

        while i < len(expr):
            if expr[i] == '(':
                depth += 1
                current.append(expr[i])
            elif expr[i] == ')':
                depth -= 1
                current.append(expr[i])
            elif depth == 0 and expr[i:i+len(operator)] == operator:
                parts.append(''.join(current).strip())
                current = []
                i += len(operator) - 1
            else:
                current.append(expr[i])
            i += 1

        if current:
            parts.append(''.join(current).strip())

        return parts if len(parts) > 1 else [expr]

    def _call_function(self, name: str) -> bool:
        """Call a built-in function."""
        if name == 'success':
            # True if all previous jobs succeeded
            return all(
                r == 'success'
                for r in self.context.job_results.values()
            )
        elif name == 'failure':
            # True if any previous job failed
            return any(
                r == 'failed'
                for r in self.context.job_results.values()
            )
        elif name == 'always':
            return True
        elif name == 'cancelled':
            return any(
                r == 'cancelled'
                for r in self.context.job_results.values()
            )
        else:
            raise ExpressionError(f"Unknown function: {name}()")

    def _evaluate_comparison(self, expr: str) -> bool:
        """Evaluate a comparison expression."""
        # Match: path == 'value' or path != 'value'
        match = re.match(
            r"([\w.]+)\s*(==|!=)\s*['\"](.+?)['\"]",
            expr
        )

        if not match:
            # Try boolean coercion
            value = self._get_value(expr)
            return bool(value)

        path, op, expected = match.groups()
        actual = self._get_value(path)

        if op == '==':
            return str(actual) == expected
        elif op == '!=':
            return str(actual) != expected

        return False

    def _get_value(self, path: str) -> Any:
        """Get a value from the context by path."""
        parts = path.split('.')

        if parts[0] == 'env':
            return self.context.env.get(parts[1], '') if len(parts) > 1 else ''

        elif parts[0] == 'needs':
            # needs.job_name.result
            if len(parts) >= 3 and parts[2] == 'result':
                job_name = parts[1]
                return self.context.job_results.get(job_name, 'unknown')
            return ''

        elif parts[0] == 'github' or parts[0] == 'ci':
            # github.event_name, github.ref, etc.
            if len(parts) > 1:
                if parts[1] == 'event_name':
                    return self.context.event
                elif parts[1] == 'ref':
                    if self.context.tag:
                        return f"refs/tags/{self.context.tag}"
                    elif self.context.branch:
                        return f"refs/heads/{self.context.branch}"
                elif parts[1] == 'branch':
                    return self.context.branch
                elif parts[1] == 'tag':
                    return self.context.tag
            return ''

        elif parts[0] == 'branch':
            return self.context.branch

        elif parts[0] == 'tag':
            return self.context.tag

        # Try variables
        return self.context.variables.get(path, '')


def evaluate_when(expression: str, context: ExecutionContext) -> bool:
    """
    Evaluate a simple 'when' expression.

    Args:
        expression: The when condition
        context: Execution context

    Returns:
        Boolean result
    """
    evaluator = SimpleExpressionEvaluator(context)
    return evaluator.evaluate(expression)


def evaluate_if(expression: str, context: ExecutionContext) -> bool:
    """
    Evaluate a complex 'if' expression.

    Args:
        expression: The if condition (may include ${{ }})
        context: Execution context

    Returns:
        Boolean result
    """
    evaluator = ComplexExpressionEvaluator(context)
    return evaluator.evaluate(expression)


def evaluate_condition(
    when: Optional[str],
    condition: Optional[str],
    context: ExecutionContext
) -> bool:
    """
    Evaluate both when and if conditions.

    Both must pass for the result to be True.

    Args:
        when: Simple when condition
        condition: Complex if condition
        context: Execution context

    Returns:
        Boolean result
    """
    # If no conditions, always run
    if not when and not condition:
        return True

    # Evaluate when condition
    if when and not evaluate_when(when, context):
        return False

    # Evaluate if condition
    if condition and not evaluate_if(condition, context):
        return False

    return True
