"""Checker for missing await on async function calls."""

import ast
import re
from pathlib import Path
from typing import List, Set

from stablestack.checkers.base import BaseChecker
from stablestack.models import Finding, Severity


class MissingAwaitChecker(BaseChecker):
    """Detects async function calls without await."""

    rule_id = "ASYNC001"
    rule_name = "missing-await"
    description = "Calling an async function without await returns a coroutine, not the result"
    why = (
        "When you call an async function without await, you get a coroutine object instead "
        "of the actual result. The code inside the function won't even run until you await it! "
        "This is a very common mistake that causes subtle bugs."
    )
    severity = Severity.ERROR
    file_extensions = {".py", ".js", ".ts", ".jsx", ".tsx", ".rs"}

    def check_file(self, path: Path, content: str) -> List[Finding]:
        if path.suffix == '.py':
            return self._check_python(path, content)
        else:
            return self._check_javascript(path, content)

    def _check_python(self, path: Path, content: str) -> List[Finding]:
        """Check Python code for missing await."""
        findings = []

        try:
            tree = ast.parse(content)
        except SyntaxError:
            return findings

        lines = content.splitlines()

        # Collect all async function names defined in this file
        async_funcs: Set[str] = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.AsyncFunctionDef):
                async_funcs.add(node.name)

        # Also add common async library functions
        async_funcs.update([
            'sleep', 'gather', 'create_task', 'wait', 'wait_for',
            'aread', 'awrite', 'aclose',
            'fetch', 'get', 'post', 'put', 'delete',  # aiohttp, httpx
        ])

        # Find calls to async functions without await
        class AwaitChecker(ast.NodeVisitor):
            def __init__(self):
                self.in_async = False
                self.awaited_calls: Set[int] = set()

            def visit_AsyncFunctionDef(self, node):
                old_in_async = self.in_async
                self.in_async = True
                self.generic_visit(node)
                self.in_async = old_in_async

            def visit_Await(self, node):
                # Mark the call inside this await as properly awaited
                if isinstance(node.value, ast.Call):
                    self.awaited_calls.add(id(node.value))
                self.generic_visit(node)

            def visit_Call(self, node):
                self.generic_visit(node)

                # Only check inside async functions
                if not self.in_async:
                    return

                # Skip if this call is awaited
                if id(node) in self.awaited_calls:
                    return

                # Get the function name
                func_name = None
                if isinstance(node.func, ast.Name):
                    func_name = node.func.id
                elif isinstance(node.func, ast.Attribute):
                    func_name = node.func.attr

                if func_name and func_name in async_funcs:
                    line_content = lines[node.lineno - 1] if node.lineno <= len(lines) else ""
                    findings.append(
                        Finding(
                            file=str(path),
                            line=node.lineno,
                            column=node.col_offset + 1,
                            code=line_content.strip(),
                            rule_id="ASYNC001",
                            rule_name="missing-await",
                            message=(
                                f"Async function '{func_name}()' called without await. "
                                "This returns a coroutine, not the result!"
                            ),
                            severity=Severity.ERROR,
                            fix_hint=f"Add await: result = await {func_name}(...)",
                            why=(
                                "When you call an async function without await, you get a coroutine "
                                "object instead of the actual result. The code inside the function "
                                "won't even run until you await it!"
                            ),
                        )
                    )

        checker = AwaitChecker()
        checker.visit(tree)

        return findings

    def _check_javascript(self, path: Path, content: str) -> List[Finding]:
        """Check JavaScript/TypeScript for missing await (basic heuristic)."""
        findings = []
        lines = content.splitlines()

        # Find async function definitions
        async_func_pattern = re.compile(r'async\s+(?:function\s+)?(\w+)')
        async_funcs = set(async_func_pattern.findall(content))

        # Add common async patterns
        async_funcs.update(['fetch', 'axios', 'request'])

        # Detect which functions are actually async by checking if they're
        # defined with 'async' keyword. Remove names that are only used as
        # sync functions (e.g. register, init) to reduce false positives.
        # We keep only functions that are explicitly defined as async in this file.
        confirmed_async: Set[str] = set()
        for line in lines:
            # Match: async function foo(, async foo(, public async foo(
            m = re.search(r'(?:^|\s)async\s+(?:function\s+)?(\w+)\s*\(', line)
            if m:
                confirmed_async.add(m.group(1))
            # Match: const foo = async (  or  const foo = async function(
            m2 = re.search(r'(?:const|let|var)\s+(\w+)\s*=\s*async\b', line)
            if m2:
                confirmed_async.add(m2.group(1))
        # Only check functions confirmed async in this file + hardcoded builtins
        async_funcs = confirmed_async | {'fetch', 'axios', 'request'}

        # Track if we're inside a Promise.all / Promise.allSettled / Promise.race
        # block — calls inside these are intentionally not awaited individually
        promise_combinator_depth = 0

        # Find calls without await in async contexts
        in_async = False
        brace_depth = 0

        for i, line in enumerate(lines):
            # Track if we're in an async function
            if 'async ' in line and ('{' in line or '=>' in line):
                in_async = True
                brace_depth = 0

            brace_depth += line.count('{') - line.count('}')
            if brace_depth <= 0:
                in_async = False

            if not in_async:
                continue

            # Track Promise combinator depth (Promise.all([, Promise.race([, etc.)
            if re.search(r'Promise\.(all|allSettled|race|any)\s*\(\s*\[', line):
                promise_combinator_depth += 1
            # Count brackets to track when we leave the combinator array
            if promise_combinator_depth > 0:
                promise_combinator_depth += line.count('[') - line.count(']')
                # The opening [ was already counted above, adjust
                if re.search(r'Promise\.(all|allSettled|race|any)\s*\(\s*\[', line):
                    promise_combinator_depth -= 1  # don't double-count opening line
                if promise_combinator_depth <= 0:
                    promise_combinator_depth = 0
                continue  # Skip checking lines inside Promise combinators

            # Check for common async calls without await
            for func in async_funcs:
                pattern = rf'\b{func}\s*\('
                match = re.search(pattern, line)
                if match and 'await' not in line and 'return ' not in line:
                    # Skip method calls — obj.foo() is likely a different function
                    # than a top-level async function foo() defined elsewhere
                    char_before = line[match.start() - 1] if match.start() > 0 else ''
                    if char_before == '.':
                        continue
                    # Skip function declarations — we want call sites, not definitions
                    # e.g. "async function foo(" or "export async function foo("
                    prefix = line[:match.start()]
                    if 'function ' in prefix:
                        continue
                    # Skip class method / shorthand async declarations
                    # e.g. "async init()" or "public async searchEmails("
                    stripped_prefix = prefix.rstrip()
                    if stripped_prefix.endswith('async'):
                        continue
                    # Skip if it's chained with .then() or .catch()
                    if '.then(' in line or '.catch(' in line:
                        continue
                    # Skip if inside Promise.all/Promise.allSettled/Promise.race
                    # (the Promise wrapper is awaited, individual calls don't need await)
                    context_window = '\n'.join(lines[max(0, i - 3):i + 1])
                    if re.search(r'Promise\.(all|allSettled|race)\s*\(', context_window):
                        continue
                    # Skip arrow functions that return the call (implicit return in handler)
                    # e.g. "handler: () => someAsyncFunc(arg)"
                    if re.search(r'=>\s*' + re.escape(func), line):
                        continue
                    # Skip if the call is inside a callback/handler object
                    # e.g. "handler: () => foo()" — the function is stored, not called immediately
                    if re.search(r':\s*(?:\([^)]*\)\s*=>|function\b)', prefix):
                        continue

                    findings.append(
                        self.create_finding(
                            path=path,
                            line=i + 1,
                            column=1,
                            code=line.strip(),
                            message=(
                                f"Async function '{func}()' may be missing await. "
                                "Without await, you get a Promise, not the result."
                            ),
                            fix_hint=f"Add await: const result = await {func}(...)",
                        )
                    )

        return findings
