/**
 * Python code for analyzing Big O complexity of user solutions.
 * Runs in Pyodide and returns an estimate with explanation.
 */

export const COMPLEXITY_ANALYZER = `
import ast
import json

class ComplexityAnalyzer(ast.NodeVisitor):
    """Analyzes Python code to estimate Big O complexity."""

    def __init__(self, source_code):
        self.source_code = source_code
        self.source_lines = source_code.split('\\n')
        self.findings = []
        self.loop_depth = 0
        self.max_loop_depth = 0
        self.has_recursion = False
        self.recursive_calls = []
        self.has_sorting = False
        self.has_linear_search = False
        self.has_binary_search_pattern = False
        self.function_name = None
        self.input_params = []
        # Track variables that are dicts or sets (O(1) lookup)
        self.hash_containers = set()
        # Track variables that are lists (O(n) lookup)
        self.list_containers = set()

    def get_line_snippet(self, lineno):
        """Get a short snippet of the line."""
        if 1 <= lineno <= len(self.source_lines):
            line = self.source_lines[lineno - 1].strip()
            if len(line) > 50:
                return line[:47] + "..."
            return line
        return ""

    def visit_FunctionDef(self, node):
        if self.function_name is None:
            self.function_name = node.name
            self.input_params = [arg.arg for arg in node.args.args]
            # Input params that look like lists
            for arg in node.args.args:
                if arg.annotation:
                    ann_str = self._node_to_str(arg.annotation)
                    if 'list' in ann_str.lower() or 'List' in ann_str:
                        self.list_containers.add(arg.arg)
        self.generic_visit(node)

    def visit_Assign(self, node):
        """Track variable assignments to identify container types."""
        for target in node.targets:
            if isinstance(target, ast.Name):
                var_name = target.id
                # Check what's being assigned
                if isinstance(node.value, ast.Dict):
                    self.hash_containers.add(var_name)
                elif isinstance(node.value, ast.Set):
                    self.hash_containers.add(var_name)
                elif isinstance(node.value, ast.Call):
                    if isinstance(node.value.func, ast.Name):
                        func_name = node.value.func.id
                        if func_name in ('dict', 'set', 'defaultdict', 'Counter', 'OrderedDict'):
                            self.hash_containers.add(var_name)
                        elif func_name == 'list':
                            self.list_containers.add(var_name)
                elif isinstance(node.value, ast.List):
                    self.list_containers.add(var_name)
        self.generic_visit(node)

    def visit_For(self, node):
        self.loop_depth += 1
        self.max_loop_depth = max(self.max_loop_depth, self.loop_depth)

        # Determine what we're iterating over
        iter_desc = self._describe_iterator(node.iter)

        self.findings.append({
            "type": "loop",
            "kind": "for",
            "line": node.lineno,
            "snippet": self.get_line_snippet(node.lineno),
            "depth": self.loop_depth,
            "iterates_over": iter_desc
        })

        self.generic_visit(node)
        self.loop_depth -= 1

    def visit_While(self, node):
        self.loop_depth += 1
        self.max_loop_depth = max(self.max_loop_depth, self.loop_depth)

        # Check for binary search pattern (low < high, left <= right, etc.)
        condition = self._get_condition_pattern(node.test)

        self.findings.append({
            "type": "loop",
            "kind": "while",
            "line": node.lineno,
            "snippet": self.get_line_snippet(node.lineno),
            "depth": self.loop_depth,
            "condition": condition
        })

        # Check body for binary search indicators (mid = ..., //= 2, etc.)
        for child in ast.walk(node):
            if isinstance(child, ast.BinOp) and isinstance(child.op, ast.FloorDiv):
                if isinstance(child.right, ast.Constant) and child.right.value == 2:
                    self.has_binary_search_pattern = True

        self.generic_visit(node)
        self.loop_depth -= 1

    def visit_Call(self, node):
        # Check for recursive calls
        if isinstance(node.func, ast.Name) and node.func.id == self.function_name:
            self.has_recursion = True
            self.recursive_calls.append({
                "line": node.lineno,
                "snippet": self.get_line_snippet(node.lineno)
            })

        # Check for sorting
        if isinstance(node.func, ast.Attribute):
            if node.func.attr in ('sort', 'sorted'):
                self.has_sorting = True
                self.findings.append({
                    "type": "operation",
                    "kind": "sort",
                    "line": node.lineno,
                    "snippet": self.get_line_snippet(node.lineno),
                    "complexity": "O(n log n)"
                })
        elif isinstance(node.func, ast.Name) and node.func.id == 'sorted':
            self.has_sorting = True
            self.findings.append({
                "type": "operation",
                "kind": "sort",
                "line": node.lineno,
                "snippet": self.get_line_snippet(node.lineno),
                "complexity": "O(n log n)"
            })

        self.generic_visit(node)

    def visit_Compare(self, node):
        # Check for "x in container" - O(1) for dict/set, O(n) for list
        for op in node.ops:
            if isinstance(op, (ast.In, ast.NotIn)):
                for comparator in node.comparators:
                    if isinstance(comparator, ast.Name):
                        var_name = comparator.id
                        # Check if it's a known hash container (O(1))
                        if var_name in self.hash_containers:
                            # O(1) lookup, no need to flag
                            pass
                        # Check if it's a known list or input param that's a list (O(n))
                        elif var_name in self.list_containers:
                            if self.loop_depth > 0:
                                self.has_linear_search = True
                                self.findings.append({
                                    "type": "operation",
                                    "kind": "linear_search",
                                    "line": node.lineno,
                                    "snippet": self.get_line_snippet(node.lineno),
                                    "note": f"'in' on list '{var_name}' is O(n)"
                                })
                        # Unknown variable - could be either, don't flag
        self.generic_visit(node)

    def _describe_iterator(self, node):
        """Describe what a for loop is iterating over."""
        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name):
                if node.func.id == 'range':
                    if node.args:
                        return self._describe_range_args(node.args)
                elif node.func.id == 'enumerate':
                    if node.args:
                        return f"enumerate({self._node_to_str(node.args[0])})"
                elif node.func.id == 'zip':
                    return "zip(...)"
        elif isinstance(node, ast.Name):
            return node.id
        elif isinstance(node, ast.Attribute):
            return f"{self._node_to_str(node.value)}.{node.attr}"
        elif isinstance(node, ast.Subscript):
            return self._node_to_str(node)
        return "iterable"

    def _describe_range_args(self, args):
        """Describe range() arguments."""
        if len(args) == 1:
            return f"range({self._node_to_str(args[0])})"
        elif len(args) >= 2:
            return f"range({self._node_to_str(args[0])}, {self._node_to_str(args[1])})"
        return "range(...)"

    def _node_to_str(self, node):
        """Convert an AST node to a simple string representation."""
        if isinstance(node, ast.Name):
            return node.id
        elif isinstance(node, ast.Constant):
            return str(node.value)
        elif isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name):
                return f"{node.func.id}(...)"
        elif isinstance(node, ast.Attribute):
            return f"{self._node_to_str(node.value)}.{node.attr}"
        elif isinstance(node, ast.BinOp):
            left = self._node_to_str(node.left)
            right = self._node_to_str(node.right)
            op = self._op_to_str(node.op)
            return f"{left} {op} {right}"
        elif isinstance(node, ast.Subscript):
            return f"{self._node_to_str(node.value)}[...]"
        return "..."

    def _op_to_str(self, op):
        """Convert operator to string."""
        ops = {
            ast.Add: "+", ast.Sub: "-", ast.Mult: "*", ast.Div: "/",
            ast.FloorDiv: "//", ast.Mod: "%", ast.Pow: "**"
        }
        return ops.get(type(op), "?")

    def _get_condition_pattern(self, node):
        """Describe a while loop condition."""
        return self._node_to_str(node)

    def analyze(self):
        """Run the analysis and return results."""
        try:
            tree = ast.parse(self.source_code)
            self.visit(tree)
        except SyntaxError as e:
            return {
                "success": False,
                "error": f"Syntax error: {e}"
            }

        # Determine overall complexity
        complexity, explanation = self._determine_complexity()

        return {
            "success": True,
            "complexity": complexity,
            "explanation": explanation,
            "details": {
                "max_loop_depth": self.max_loop_depth,
                "has_recursion": self.has_recursion,
                "has_sorting": self.has_sorting,
                "has_binary_search_pattern": self.has_binary_search_pattern,
                "findings": self.findings,
                "recursive_calls": self.recursive_calls
            }
        }

    def _determine_complexity(self):
        """Determine the overall complexity based on findings."""
        explanations = []
        complexity_factors = []

        # Handle recursion
        if self.has_recursion:
            if self.has_binary_search_pattern:
                complexity_factors.append(("log n", "binary search recursion"))
                explanations.append("Recursion with halving pattern suggests O(log n)")
            else:
                complexity_factors.append(("?", "recursion detected"))
                explanations.append(f"Recursion found - complexity depends on recurrence relation")
                for rc in self.recursive_calls:
                    explanations.append(f"  Line {rc['line']}: {rc['snippet']}")

        # Handle loops
        if self.max_loop_depth > 0:
            loop_findings = [f for f in self.findings if f["type"] == "loop"]

            if self.has_binary_search_pattern and self.max_loop_depth == 1:
                complexity_factors.append(("log n", "binary search loop"))
                explanations.append("While loop with halving pattern: O(log n)")
            else:
                # Describe each loop level
                for finding in loop_findings:
                    snippet = finding.get("snippet", "")
                    depth_info = f" (nested, depth {finding['depth']})" if finding['depth'] > 1 else ""
                    if finding["kind"] == "for":
                        iter_over = finding.get("iterates_over", "iterable")
                        explanations.append(
                            f"Loop over {iter_over}{depth_info} → O(n)"
                        )
                        if snippet:
                            explanations.append(f"  └─ {snippet}")
                    else:
                        condition = finding.get("condition", "...")
                        explanations.append(
                            f"While loop{depth_info}"
                        )
                        if snippet:
                            explanations.append(f"  └─ {snippet}")

                # Estimate based on nesting
                if self.max_loop_depth == 1:
                    complexity_factors.append(("n", "single loop"))
                elif self.max_loop_depth == 2:
                    complexity_factors.append(("n²", "nested loops"))
                elif self.max_loop_depth == 3:
                    complexity_factors.append(("n³", "triple nested loops"))
                else:
                    complexity_factors.append((f"n^{self.max_loop_depth}", f"{self.max_loop_depth} nested loops"))

        # Handle sorting
        if self.has_sorting:
            complexity_factors.append(("n log n", "sorting"))
            sort_findings = [f for f in self.findings if f.get("kind") == "sort"]
            for sf in sort_findings:
                explanations.append(f"Line {sf['line']}: sorting operation O(n log n)")

        # Handle linear search in loop
        if self.has_linear_search and self.max_loop_depth >= 1:
            explanations.append("Note: 'in' operator on list inside loop adds O(n) per iteration")
            # This could bump n to n², but let's just note it

        # Combine factors to get final complexity
        if not complexity_factors:
            return "O(1)", ["No loops or recursion detected - constant time"]

        # Take the dominant term
        complexity = self._get_dominant_complexity(complexity_factors)

        return f"O({complexity})", explanations

    def _get_dominant_complexity(self, factors):
        """Get the dominant complexity from a list of factors."""
        # Priority order (higher = more complex)
        priority = {
            "1": 0, "log n": 1, "n": 2, "n log n": 3,
            "n²": 4, "n³": 5, "n^4": 6, "2^n": 7, "n!": 8, "?": 10
        }

        dominant = "1"
        for factor, _ in factors:
            if factor.startswith("n^"):
                try:
                    exp = int(factor[2:])
                    if exp > priority.get(dominant, 0):
                        dominant = factor
                except:
                    pass
            elif priority.get(factor, 0) > priority.get(dominant, 0):
                dominant = factor

        # Handle sorting + loops
        has_sort = any(f[0] == "n log n" for f in factors)
        has_n2 = any(f[0] == "n²" for f in factors)

        if has_sort and not has_n2 and dominant == "n":
            return "n log n"

        return dominant


def analyze_complexity(code):
    """Main entry point for complexity analysis."""
    analyzer = ComplexityAnalyzer(code)
    return analyzer.analyze()
`.trim();

export interface ComplexityResult {
  success: boolean;
  error?: string;
  complexity?: string;
  explanation?: string[];
  details?: {
    max_loop_depth: number;
    has_recursion: boolean;
    has_sorting: boolean;
    has_binary_search_pattern: boolean;
    findings: Array<{
      type: string;
      kind: string;
      line: number;
      snippet: string;
      depth?: number;
      iterates_over?: string;
      condition?: string;
      complexity?: string;
      note?: string;
    }>;
    recursive_calls: Array<{
      line: number;
      snippet: string;
    }>;
  };
}
