from .parser import (
    Assignment,
    BinaryExpression,
    BreakStatement,
    ContinueStatement,
    ForLoop,
    FunctionCall,
    FunctionDeclaration,
    FormattedString,
    Identifier,
    IndexAssignment,
    IndexExpression,
    IfStatement,
    ListLiteral,
    Literal,
    ObjectLiteral,
    PrintStatement,
    Program,
    ReturnStatement,
    ThrowStatement,
    TryStatement,
    UnaryExpression,
    WhileStatement,
)
import os
import random
import time

from .environment import Environment
from .builtin_modules import (
    BuiltinFunction,
    BuiltinModuleError,
    is_builtin_module,
    load_builtin_module,
)


class InterpreterError(Exception):
    """Raised for runtime errors in the Prasad interpreter."""


class _BreakSignal(Exception):
    pass


class _ContinueSignal(Exception):
    pass


class _ReturnSignal(Exception):
    def __init__(self, value):
        self.value = value


class PrasadThrow(Exception):
    def __init__(self, message, line=None):
        super().__init__(message)
        self.message = message
        self.line = line

    def __str__(self):
        if self.line is not None:
            return f"Chuk: {self.message}\nLine: {self.line}"
        return f"Chuk: {self.message}"


class ModuleInstance:
    def __init__(self, *, name, path, env: Environment, exports: dict):
        self.name = name
        self.path = path
        self.env = env
        self.exports = exports


class Interpreter:
    _MODULE_IN_PROGRESS = object()

    def __init__(self, *, base_dir=None, module_cache=None):
        self.env = Environment()
        self.base_dir = base_dir or os.getcwd()
        self.module_cache = module_cache if module_cache is not None else {}
        self._timers = {}
        self._next_timer_id = 1

    def _register_timer(self, handle):
        timer_id = self._next_timer_id
        self._next_timer_id += 1
        self._timers[timer_id] = handle
        return timer_id

    def _pop_timer(self, timer_id):
        return self._timers.pop(timer_id, None)

    @staticmethod
    def _type_name(value):
        if value is None:
            return "kahi"
        if isinstance(value, bool):
            return "boolean"
        if isinstance(value, int):
            return "int"
        if isinstance(value, float):
            return "float"
        if isinstance(value, str):
            return "string"
        if isinstance(value, list):
            return "list"
        return type(value).__name__

    def _runtime_error(self, message, node=None):
        """Format runtime error messages with optional line information."""
        if node is not None and getattr(node, "line", None) is not None:
            return InterpreterError(f"Chuk: {message}\nLine: {node.line}")
        return InterpreterError(f"Chuk: {message}")

    def run(self, program: Program):
        try:
            for stmt in program.statements:
                self.exec_stmt(stmt)
        except _BreakSignal:
            raise self._runtime_error("'todu' can only be used inside a loop")
        except _ContinueSignal:
            raise self._runtime_error("'pudhe' can only be used inside a loop")
        except _ReturnSignal:
            raise self._runtime_error("'parat' can only be used inside a function")

    def exec_stmt(self, stmt):
        if isinstance(stmt, Assignment):
            self.eval_expr(stmt)
            return None

        if isinstance(stmt, IndexAssignment):
            self.eval_expr(stmt)
            return None

        if isinstance(stmt, PrintStatement):
            values = [self.eval_expr(arg) for arg in stmt.args]
            print(*values)
            return None

        if isinstance(stmt, IfStatement):
            cond = self.eval_expr(stmt.condition)
            body = stmt.body if self.is_truthy(cond) else (stmt.else_body or [])
            self._exec_block(body, self.env, propagate_assignments=True)
            return None

        if isinstance(stmt, WhileStatement):
            parent_env = self.env
            old_env = self.env
            self.env = Environment(parent=parent_env, propagate_assignments=True)
            try:
                while self.is_truthy(self.eval_expr(stmt.condition)):
                    try:
                        for inner in stmt.body:
                            self.exec_stmt(inner)
                    except _ContinueSignal:
                        continue
                    except _BreakSignal:
                        break
            finally:
                self.env = old_env
            return None

        if isinstance(stmt, ForLoop):
            parent_env = self.env
            old_env = self.env
            self.env = Environment(parent=parent_env, propagate_assignments=True)
            try:
                while self.is_truthy(self.eval_expr(stmt.condition)):
                    try:
                        for inner in stmt.body:
                            self.exec_stmt(inner)
                    except _ContinueSignal:
                        continue
                    except _BreakSignal:
                        break
            finally:
                self.env = old_env
            return None

        if isinstance(stmt, BreakStatement):
            raise _BreakSignal()

        if isinstance(stmt, ContinueStatement):
            raise _ContinueSignal()

        if isinstance(stmt, FunctionDeclaration):
            self.env.define(stmt.name, stmt)
            return None

        if isinstance(stmt, ReturnStatement):
            raise _ReturnSignal(self.eval_expr(stmt.value))

        if isinstance(stmt, ThrowStatement):
            value = self.eval_expr(stmt.value)
            raise PrasadThrow(str(value), line=getattr(stmt, "line", None))

        if isinstance(stmt, TryStatement):
            self._exec_try(stmt)
            return None

        if isinstance(stmt, FunctionCall):
            # Statement-level call (e.g. foo(...);). Support later.
            self.eval_expr(stmt)
            return None

        raise self._runtime_error(
            f"Unknown statement type: {type(stmt).__name__}", stmt
        )

    def _exec_block(self, statements, parent_env, *, propagate_assignments=True):
        old_env = self.env
        self.env = Environment(parent=parent_env, propagate_assignments=propagate_assignments)
        try:
            for inner in statements:
                self.exec_stmt(inner)
        finally:
            self.env = old_env

    def _exec_try(self, stmt: TryStatement):
        parent_env = self.env
        pending = None

        def error_message(err):
            if isinstance(err, PrasadThrow):
                return err.message
            text = str(err)
            if text.startswith("Chuk: "):
                text = text[len("Chuk: ") :]
            if "\nLine:" in text:
                text = text.split("\nLine:", 1)[0].strip()
            return text

        try:
            try:
                self._exec_block(stmt.try_body, parent_env, propagate_assignments=True)
            except Exception as e:
                if isinstance(e, (_BreakSignal, _ContinueSignal, _ReturnSignal)):
                    raise
                if stmt.catch_body is None:
                    pending = e
                else:
                    old_env = self.env
                    catch_env = Environment(parent=parent_env, propagate_assignments=True)
                    if stmt.catch_name:
                        catch_env.define(stmt.catch_name, error_message(e))
                    self.env = catch_env
                    try:
                        for inner in stmt.catch_body:
                            self.exec_stmt(inner)
                    except Exception as e2:
                        pending = e2
                    finally:
                        self.env = old_env
        except (_BreakSignal, _ContinueSignal, _ReturnSignal) as signal:
            pending = signal
        finally:
            if stmt.finally_body is not None:
                try:
                    self._exec_block(stmt.finally_body, parent_env, propagate_assignments=True)
                except Exception as fin_e:
                    pending = fin_e

        if pending is not None:
            raise pending

    def eval_expr(self, expr):
        if isinstance(expr, Assignment):
            value = self.eval_expr(expr.value)
            self.env.assign(expr.name, value)
            return value

        if isinstance(expr, IndexAssignment):
            target = self.eval_expr(expr.target)
            index = self.eval_expr(expr.index)
            value = self.eval_expr(expr.value)

            if isinstance(target, list):
                if not isinstance(index, int):
                    raise self._runtime_error("List index must be a number", expr)
                try:
                    target[index] = value
                except Exception as e:
                    raise self._runtime_error(str(e), expr)
                return value

            if isinstance(target, dict):
                if not isinstance(index, str):
                    raise self._runtime_error("Object key must be a string", expr)
                target[index] = value
                return value

            raise self._runtime_error(
                "Index assignment is only supported on lists and objects", expr
            )

        if isinstance(expr, Literal):
            return expr.value

        if isinstance(expr, FormattedString):
            return self._eval_formatted_string(expr)

        if isinstance(expr, ListLiteral):
            return [self.eval_expr(el) for el in expr.elements]

        if isinstance(expr, ObjectLiteral):
            result = {}
            for key, value_expr in expr.entries:
                result[key] = self.eval_expr(value_expr)
            return result

        if isinstance(expr, IndexExpression):
            target = self.eval_expr(expr.target)
            index = self.eval_expr(expr.index)

            if isinstance(target, list):
                if not isinstance(index, int):
                    raise self._runtime_error("List index must be a number", expr)
                try:
                    return target[index]
                except Exception as e:
                    raise self._runtime_error(str(e), expr)

            if isinstance(target, dict):
                if not isinstance(index, str):
                    raise self._runtime_error("Object key must be a string", expr)
                try:
                    return target[index]
                except KeyError:
                    raise self._runtime_error(f"Key not found '{index}'", expr)

            raise self._runtime_error(
                "Indexing is only supported on lists and objects", expr
            )

        if isinstance(expr, Identifier):
            try:
                return self.env.get(expr.name)
            except KeyError:
                raise self._runtime_error(f"Undefined variable '{expr.name}'", expr)

        if isinstance(expr, UnaryExpression):
            right = self.eval_expr(expr.expr)
            if expr.op == "-":
                return -right
            if expr.op == "+":
                return +right
            if expr.op == "!":
                return not self.is_truthy(right)
            raise self._runtime_error(f"Unknown unary operator '{expr.op}'", expr)

        if isinstance(expr, BinaryExpression):
            op = expr.op

            if op == "&&":
                left = self.eval_expr(expr.left)
                if not self.is_truthy(left):
                    return left
                return self.eval_expr(expr.right)

            if op == "||":
                left = self.eval_expr(expr.left)
                if self.is_truthy(left):
                    return left
                return self.eval_expr(expr.right)

            left = self.eval_expr(expr.left)
            right = self.eval_expr(expr.right)

            if op == "+":
                if isinstance(left, str) and isinstance(right, str):
                    return left + right
                if isinstance(left, (int, float)) and isinstance(right, (int, float)):
                    return left + right
                if isinstance(left, list) and isinstance(right, list):
                    return left + right
                raise self._runtime_error(
                    f"Type mismatch for '+': {self._type_name(left)} + {self._type_name(right)}",
                    expr,
                )
            if op == "-":
                if isinstance(left, (int, float)) and isinstance(right, (int, float)):
                    return left - right
                raise self._runtime_error(
                    f"Type mismatch for '-': {self._type_name(left)} - {self._type_name(right)}",
                    expr,
                )
            if op == "*":
                if isinstance(left, (int, float)) and isinstance(right, (int, float)):
                    return left * right
                raise self._runtime_error(
                    f"Type mismatch for '*': {self._type_name(left)} * {self._type_name(right)}",
                    expr,
                )
            if op == "/":
                if isinstance(left, (int, float)) and isinstance(right, (int, float)):
                    return left / right
                raise self._runtime_error(
                    f"Type mismatch for '/': {self._type_name(left)} / {self._type_name(right)}",
                    expr,
                )
            if op == "%":
                if isinstance(left, (int, float)) and isinstance(right, (int, float)):
                    return left % right
                raise self._runtime_error(
                    f"Type mismatch for '%': {self._type_name(left)} % {self._type_name(right)}",
                    expr,
                )
            if op == "==":
                return left == right
            if op == "!=":
                return left != right
            if op == ">":
                try:
                    return left > right
                except TypeError:
                    raise self._runtime_error(
                        f"Type mismatch for '>': {self._type_name(left)} > {self._type_name(right)}",
                        expr,
                    )
            if op == "<":
                try:
                    return left < right
                except TypeError:
                    raise self._runtime_error(
                        f"Type mismatch for '<': {self._type_name(left)} < {self._type_name(right)}",
                        expr,
                    )
            if op == ">=":
                try:
                    return left >= right
                except TypeError:
                    raise self._runtime_error(
                        f"Type mismatch for '>=': {self._type_name(left)} >= {self._type_name(right)}",
                        expr,
                    )
            if op == "<=":
                try:
                    return left <= right
                except TypeError:
                    raise self._runtime_error(
                        f"Type mismatch for '<=': {self._type_name(left)} <= {self._type_name(right)}",
                        expr,
                    )

            raise self._runtime_error(f"Unknown binary operator '{op}'", expr)

        if isinstance(expr, FunctionCall):
            if expr.name == "vichar":
                if len(expr.args) == 0:
                    return input()
                elif len(expr.args) == 1:
                    prompt = self.eval_expr(expr.args[0])
                    return input(prompt)
                else:
                    raise self._runtime_error("vichar takes 0 or 1 argument", expr)
            if expr.name == "random":
                if len(expr.args) != 0:
                    raise self._runtime_error("random takes 0 arguments", expr)
                return random.random()
            if expr.name == "randint":
                if len(expr.args) != 2:
                    raise self._runtime_error("randint takes exactly 2 arguments", expr)
                a = self.eval_expr(expr.args[0])
                b = self.eval_expr(expr.args[1])
                if not isinstance(a, int) or isinstance(a, bool):
                    raise self._runtime_error("randint expects integer arguments", expr)
                if not isinstance(b, int) or isinstance(b, bool):
                    raise self._runtime_error("randint expects integer arguments", expr)
                return random.randint(a, b)
            if expr.name == "choice":
                if len(expr.args) != 1:
                    raise self._runtime_error("choice takes exactly 1 argument", expr)
                seq = self.eval_expr(expr.args[0])
                if not isinstance(seq, list):
                    raise self._runtime_error("choice expects a list", expr)
                if len(seq) == 0:
                    raise self._runtime_error("choice expects a non-empty list", expr)
                return random.choice(seq)
            if expr.name == "aaram":
                if len(expr.args) != 1:
                    raise self._runtime_error("aaram takes exactly 1 argument", expr)
                seconds = self.eval_expr(expr.args[0])
                if isinstance(seconds, bool) or not isinstance(seconds, (int, float)):
                    raise self._runtime_error("aaram expects a number of seconds", expr)
                if seconds < 0:
                    raise self._runtime_error("aaram expects non-negative seconds", expr)
                time.sleep(seconds)
                return None
            if expr.name == "wacha":
                if len(expr.args) != 1:
                    raise self._runtime_error("wacha takes exactly 1 argument", expr)
                path = self.eval_expr(expr.args[0])
                if not isinstance(path, str):
                    raise self._runtime_error("wacha expects a string file path", expr)
                full_path = path if os.path.isabs(path) else os.path.join(self.base_dir, path)
                try:
                    with open(full_path, "r", encoding="utf-8") as f:
                        return f.read()
                except FileNotFoundError:
                    raise self._runtime_error(f"File not found '{path}'", expr)
                except Exception as e:
                    raise self._runtime_error(str(e), expr)
            if expr.name == "lih":
                if len(expr.args) != 2:
                    raise self._runtime_error("lih takes exactly 2 arguments", expr)
                path = self.eval_expr(expr.args[0])
                content = self.eval_expr(expr.args[1])
                if not isinstance(path, str):
                    raise self._runtime_error("lih expects a string file path", expr)
                if not isinstance(content, str):
                    raise self._runtime_error("lih expects content to be a string", expr)
                full_path = path if os.path.isabs(path) else os.path.join(self.base_dir, path)
                try:
                    with open(full_path, "w", encoding="utf-8") as f:
                        f.write(content)
                except Exception as e:
                    raise self._runtime_error(str(e), expr)
                return None
            if expr.name in ("lambi", "lamba"):
                if len(expr.args) != 1:
                    raise self._runtime_error("lambi takes exactly 1 argument", expr)
                value = self.eval_expr(expr.args[0])
                if not isinstance(value, list):
                    raise self._runtime_error("lambi expects a list", expr)
                return len(value)
            if expr.name == "ghya":
                if len(expr.args) != 1:
                    raise self._runtime_error("ghya takes exactly 1 argument", expr)
                spec = self.eval_expr(expr.args[0])
                if not isinstance(spec, str):
                    raise self._runtime_error("ghya expects a string module path", expr)
                exports = self._import_module(spec, expr)
                return exports
            if expr.name == "taka":
                if len(expr.args) != 2:
                    raise self._runtime_error("taka takes exactly 2 arguments", expr)
                target = self.eval_expr(expr.args[0])
                if not isinstance(target, list):
                    raise self._runtime_error("taka expects a list as first argument", expr)
                value = self.eval_expr(expr.args[1])
                target.append(value)
                return None
            if expr.name == "kadha":
                if len(expr.args) != 1:
                    raise self._runtime_error("kadha takes exactly 1 argument", expr)
                target = self.eval_expr(expr.args[0])
                if not isinstance(target, list):
                    raise self._runtime_error("kadha expects a list", expr)
                if len(target) == 0:
                    raise self._runtime_error("Cannot kadha from empty list", expr)
                return target.pop()
            try:
                callee = self.env.get(expr.name)
            except KeyError:
                callee = None

            if callee is None and "." in expr.name:
                base, member = expr.name.split(".", 1)
                try:
                    base_value = self.env.get(base)
                except KeyError:
                    base_value = None

                if isinstance(base_value, dict) and member in base_value:
                    callee = base_value[member]
                elif is_builtin_module(base):
                    exports = self._import_module(base, expr)
                    if member not in exports:
                        raise self._runtime_error(
                            f"Unknown function '{expr.name}'", expr
                        )
                    callee = exports[member]

            arg_values = [self.eval_expr(arg) for arg in expr.args]
            try:
                return self._call_value(callee, arg_values, expr, name_hint=expr.name)
            except BuiltinModuleError as e:
                raise self._runtime_error(str(e), expr)
            except TypeError as e:
                raise self._runtime_error(str(e), expr)
            # Builtins and user functions can be added here.
            raise self._runtime_error(f"Unknown function '{expr.name}'", expr)

        raise self._runtime_error(
            f"Unknown expression type: {type(expr).__name__}", expr
        )

    def _eval_formatted_string(self, node: FormattedString):
        template = node.template
        out = []
        i = 0

        while i < len(template):
            ch = template[i]
            if ch == "{":
                if i + 1 < len(template) and template[i + 1] == "{":
                    out.append("{")
                    i += 2
                    continue

                end = template.find("}", i + 1)
                if end == -1:
                    raise self._runtime_error("Unterminated '{' in formatted string", node)

                expr_src = template[i + 1 : end].strip()
                if not expr_src:
                    raise self._runtime_error("Empty '{}' in formatted string", node)

                try:
                    from .lexer import Lexer
                    from .parser import ParseError, Parser
                    from .tokens import TokenType

                    tokens = Lexer(expr_src).tokenize()
                    parser = Parser(tokens, source=expr_src)
                    inner_expr = parser.expression()
                    parser.maybe_semicolon()
                    if parser.current().type != TokenType.EOF:
                        raise ParseError("Unexpected extra input")

                    value = self.eval_expr(inner_expr)
                except Exception as e:
                    raise self._runtime_error(
                        f"Invalid expression in formatted string: {e}", node
                    )

                out.append(str(value))
                i = end + 1
                continue

            if ch == "}":
                if i + 1 < len(template) and template[i + 1] == "}":
                    out.append("}")
                    i += 2
                    continue
                raise self._runtime_error("Single '}' in formatted string", node)

            out.append(ch)
            i += 1

        return "".join(out)

    @staticmethod
    def is_truthy(value):
        if value is None:
            return False
        if value is False:
            return False
        if value is True:
            return True
        if isinstance(value, bool):
            return bool(value)
        if isinstance(value, (int, float)):
            return value != 0
        if isinstance(value, str):
            return value != ""
        return True

    def _call_user_function(self, func: FunctionDeclaration, args: list, node=None, name_hint=None):
        name = name_hint or getattr(func, "name", "<function>")
        if len(args) != len(func.params):
            raise self._runtime_error(f"Wrong number of arguments for '{name}'", node)

        old_env = self.env
        call_env = Environment(parent=old_env, propagate_assignments=False)
        for param, value in zip(func.params, args):
            call_env.define(param, value)

        self.env = call_env
        try:
            try:
                for stmt in func.body:
                    self.exec_stmt(stmt)
            except _ReturnSignal as rs:
                return rs.value
            return None
        finally:
            self.env = old_env

    def _call_value(self, callee, args: list, node=None, name_hint=None):
        if isinstance(callee, FunctionDeclaration):
            return self._call_user_function(callee, args, node=node, name_hint=name_hint)
        if isinstance(callee, BuiltinFunction):
            return callee.call(self, args, node=node)
        if callable(callee):
            return callee(*args)
        raise self._runtime_error(f"Unknown function '{name_hint or callee}'", node)

    def _resolve_module_path(self, spec: str):
        ext = os.path.splitext(spec)[1]
        path = spec if ext else f"{spec}.prasad"
        if not os.path.isabs(path):
            path = os.path.join(self.base_dir, path)
        return os.path.abspath(os.path.normpath(path))

    def _import_module(self, spec: str, node):
        from .lexer import Lexer
        from .parser import Parser

        if is_builtin_module(spec):
            key = f"<builtin:{spec}>"
            cached = self.module_cache.get(key)
            if cached is None:
                exports = load_builtin_module(spec)
                module_env = Environment(parent=None, propagate_assignments=False)
                module_env.values.update(exports)
                cached = ModuleInstance(name=spec, path=key, env=module_env, exports=exports)
                self.module_cache[key] = cached

            exports = cached.exports if isinstance(cached, ModuleInstance) else cached
            self.env.define(spec, exports)
            for name, value in exports.items():
                if not name.startswith("_"):
                    self.env.define(name, value)
            return exports

        path = self._resolve_module_path(spec)

        cached = self.module_cache.get(path)
        if cached is self._MODULE_IN_PROGRESS:
            raise self._runtime_error(f"Circular import detected for '{spec}'", node)

        if isinstance(cached, ModuleInstance):
            exports = cached.exports
        elif cached is None:
            self.module_cache[path] = self._MODULE_IN_PROGRESS

            old_env = self.env
            old_base_dir = self.base_dir

            module_env = Environment(parent=None, propagate_assignments=False)
            self.env = module_env
            self.base_dir = os.path.dirname(path)

            try:
                with open(path, "r", encoding="utf-8") as f:
                    source = f.read()
                program = Parser(Lexer(source).tokenize(), source=source).parse()
                self.run(program)
            except FileNotFoundError:
                self.module_cache.pop(path, None)
                raise self._runtime_error(f"Module not found '{spec}'", node)
            except Exception:
                self.module_cache.pop(path, None)
                raise
            finally:
                self.env = old_env
                self.base_dir = old_base_dir

            exports = {
                name: value
                for name, value in module_env.values.items()
                if not name.startswith("_")
            }
            instance = ModuleInstance(name=spec, path=path, env=module_env, exports=exports)
            self.module_cache[path] = instance
        else:
            # Backward-compat: older cache values may be plain dict exports.
            exports = cached

        # Expose module variables automatically in the current scope.
        self.env.define(spec, exports)
        for name, value in exports.items():
            self.env.define(name, value)

        return exports
