#!/usr/bin/env python3
"""
MD — Murder Drones language
Cyn is the boss. The Absolute Solver compiles your directives.
"""
from __future__ import annotations

import argparse
import math
import os
import re
import sys
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple

VERSION = "0.1.0"
CYN = "Cyn"

KEYWORDS = {
    "spawn", "drone", "core", "hive", "dissolve", "reboot", "corrupt",
    "terminate", "directive", "ascend", "reformat", "absolute", "void",
    "and", "or", "not", "true", "false",
}

TOKEN_RE = re.compile(
    r'"(?:\\.|[^"\\])*"|'
    r"'(?:\\.|[^'\\])*'|"
    r"//[^\n]*|"
    r"\d+\.\d+|\d+|"
    r"[a-zA-Z_][a-zA-Z0-9_]*|"
    r"==|!=|<=|>=|=>|[+\-*/%<>=!&|(),;{}.\[\]]|"
    r"\s+"
)


@dataclass
class Token:
    kind: str
    value: str
    line: int
    col: int


class CynError(Exception):
    def __init__(self, msg: str, line: int = 0, col: int = 0):
        self.line = line
        self.col = col
        super().__init__(msg)

    def __str__(self) -> str:
        loc = f" (line {self.line}, col {self.col})" if self.line else ""
        return f"[{CYN}] {self.args[0]}{loc}"


def cyn(msg: str, line: int = 0, col: int = 0) -> CynError:
    return CynError(msg, line, col)


def lex(source: str, path: str = "<stdin>") -> List[Token]:
    tokens: List[Token] = []
    line, col = 1, 1
    i = 0
    while i < len(source):
        m = TOKEN_RE.match(source, i)
        if not m:
            raise cyn(f"Unauthorized character '{source[i]}' — Cyn rejects this unit.", line, col)
        raw = m.group(0)
        i = m.end()
        if raw.isspace():
            for ch in raw:
                if ch == "\n":
                    line += 1
                    col = 1
                else:
                    col += 1
            continue
        if raw.startswith("//"):
            line += raw.count("\n")
            col = len(raw.rsplit("\n", 1)[-1]) + 1 if "\n" in raw else col + len(raw)
            continue
        kind = "ident"
        if raw in KEYWORDS:
            kind = "kw"
        elif raw[0] in "\"'":
            kind = "str"
        elif re.fullmatch(r"\d+\.\d+|\d+", raw):
            kind = "num"
        elif raw in "(),;{}.[]":
            kind = raw
        elif raw in ("==", "!=", "<=", ">=", "=>", "+", "-", "*", "/", "%", "<", ">", "=", "!", "&", "|"):
            kind = "op"
        tokens.append(Token(kind, raw, line, col))
        if "\n" in raw:
            parts = raw.split("\n")
            line += len(parts) - 1
            col = len(parts[-1]) + 1
        else:
            col += len(raw)
    tokens.append(Token("eof", "", line, col))
    return tokens


# AST nodes
@dataclass
class Program:
    stmts: List[Any]


@dataclass
class Block:
    stmts: List[Any]


@dataclass
class Spawn:
    expr: Any
    line: int


@dataclass
class Core:
    name: str
    expr: Any
    line: int


@dataclass
class Drone:
    name: str
    params: List[str]
    body: Block
    line: int


@dataclass
class Hive:
    cond: Any
    then: Block
    alt: Optional[Block]
    line: int


@dataclass
class Reboot:
    cond: Any
    body: Block
    line: int


@dataclass
class Corrupt:
    var: str
    start: Any
    end: Any
    body: Block
    line: int


@dataclass
class Terminate:
    expr: Optional[Any]
    line: int


@dataclass
class Ascend:
    line: int


@dataclass
class Reformat:
    line: int


@dataclass
class ExprStmt:
    expr: Any
    line: int


@dataclass
class Directive:
    path: str
    line: int


@dataclass
class Assign:
    name: str
    expr: Any
    line: int


@dataclass
class BinOp:
    op: str
    left: Any
    right: Any
    line: int


@dataclass
class Unary:
    op: str
    expr: Any
    line: int


@dataclass
class Call:
    callee: Any
    args: List[Any]
    line: int


@dataclass
class Index:
    obj: Any
    index: Any
    line: int


@dataclass
class Get:
    obj: Any
    name: str
    line: int


@dataclass
class Literal:
    value: Any
    line: int


@dataclass
class Var:
    name: str
    line: int


@dataclass
class ListLit:
    items: List[Any]
    line: int


class Parser:
    def __init__(self, tokens: List[Token], path: str):
        self.tokens = tokens
        self.path = path
        self.pos = 0

    def cur(self) -> Token:
        return self.tokens[self.pos]

    def peek(self, n: int = 1) -> Token:
        return self.tokens[min(self.pos + n, len(self.tokens) - 1)]

    def eat(self, kind: Optional[str] = None, value: Optional[str] = None) -> Token:
        t = self.cur()
        if value is not None:
            if t.value != value:
                raise cyn(f"Expected '{value}', got '{t.value}'.", t.line, t.col)
        elif kind and t.kind != kind:
            raise cyn(f"Expected {kind}, got '{t.value}'. Cyn demands order.", t.line, t.col)
        self.pos += 1
        return t

    def match(self, value: str) -> bool:
        if self.cur().value == value:
            self.pos += 1
            return True
        return False

    def parse(self) -> Program:
        stmts = []
        while self.cur().kind != "eof":
            stmts.append(self.stmt())
        return Program(stmts)

    def block(self) -> Block:
        self.eat("{")
        stmts = []
        while not self.match("}"):
            if self.cur().kind == "eof":
                raise cyn("Unclosed block — the hive never dissolved.", self.cur().line, self.cur().col)
            stmts.append(self.stmt())
        return Block(stmts)

    def stmt(self) -> Any:
        t = self.cur()
        if t.value == "spawn":
            self.eat("kw")
            return Spawn(self.expr(), t.line)
        if t.value == "core":
            self.eat("kw")
            name = self.eat("ident").value
            self.eat("op", "=")
            return Core(name, self.expr(), t.line)
        if t.value == "drone":
            return self.drone_def()
        if t.value == "hive":
            return self.hive_stmt()
        if t.value == "reboot":
            return self.reboot_stmt()
        if t.value == "corrupt":
            return self.corrupt_stmt()
        if t.value == "terminate":
            self.eat("kw")
            expr = None
            if self.cur().value not in (";", "}"):
                expr = self.expr()
            self.match(";")
            return Terminate(expr, t.line)
        if t.value == "ascend":
            self.eat("kw")
            self.match(";")
            return Ascend(t.line)
        if t.value == "reformat":
            self.eat("kw")
            self.match(";")
            return Reformat(t.line)
        if t.value == "directive":
            self.eat("kw")
            path_tok = self.cur()
            if path_tok.kind != "str":
                raise cyn("Directive path must be a string — Cyn approves imports only with quotes.", path_tok.line, path_tok.col)
            self.pos += 1
            self.match(";")
            return Directive(path_tok.value[1:-1], t.line)
        if t.value == "{":
            return self.block()
        if t.kind == "ident" and self.peek().value == "=":
            name = self.eat("ident").value
            self.eat("op", "=")
            return Assign(name, self.expr(), t.line)
        return ExprStmt(self.expr(), t.line)

    def drone_def(self) -> Drone:
        t = self.eat("kw")
        name = self.eat("ident").value
        self.eat("(")
        params: List[str] = []
        if not self.match(")"):
            params.append(self.eat("ident").value)
            while self.match(","):
                params.append(self.eat("ident").value)
            self.eat(")")
        body = self.block()
        return Drone(name, params, body, t.line)

    def hive_stmt(self) -> Hive:
        t = self.eat("kw")
        self.eat("(")
        cond = self.expr()
        self.eat(")")
        then = self.block()  # hive body
        alt = None
        if self.match("dissolve"):
            alt = self.block()
        return Hive(cond, then, alt, t.line)

    def reboot_stmt(self) -> Reboot:
        t = self.eat("kw")
        self.eat("(")
        cond = self.expr()
        self.eat(")")
        body = self.block()
        return Reboot(cond, body, t.line)

    def corrupt_stmt(self) -> Corrupt:
        t = self.eat("kw")
        self.eat("(")
        var = self.eat("ident").value
        self.eat("op", "=")
        start = self.expr()
        self.eat("op", "=>")
        end = self.expr()
        self.eat(")")
        body = self.block()
        return Corrupt(var, start, end, body, t.line)

    def expr(self) -> Any:
        return self.logic_or()

    def logic_or(self) -> Any:
        left = self.logic_and()
        while self.cur().value == "or":
            op = self.eat("kw").value
            right = self.logic_and()
            left = BinOp(op, left, right, left.line if hasattr(left, "line") else self.cur().line)
        return left

    def logic_and(self) -> Any:
        left = self.equality()
        while self.cur().value == "and":
            op = self.eat("kw").value
            right = self.equality()
            left = BinOp(op, left, right, left.line if hasattr(left, "line") else self.cur().line)
        return left

    def equality(self) -> Any:
        left = self.compare()
        while self.cur().value in ("==", "!="):
            op = self.eat("op").value
            right = self.compare()
            left = BinOp(op, left, right, left.line if hasattr(left, "line") else self.cur().line)
        return left

    def compare(self) -> Any:
        left = self.add()
        while self.cur().value in ("<", ">", "<=", ">="):
            op = self.eat("op").value
            right = self.add()
            left = BinOp(op, left, right, left.line if hasattr(left, "line") else self.cur().line)
        return left

    def add(self) -> Any:
        left = self.mul()
        while self.cur().value in ("+", "-"):
            op = self.eat("op").value
            right = self.mul()
            left = BinOp(op, left, right, left.line if hasattr(left, "line") else self.cur().line)
        return left

    def mul(self) -> Any:
        left = self.unary()
        while self.cur().value in ("*", "/", "%"):
            op = self.eat("op").value
            right = self.unary()
            left = BinOp(op, left, right, left.line if hasattr(left, "line") else self.cur().line)
        return left

    def unary(self) -> Any:
        if self.cur().value in ("-", "not"):
            op = self.eat("kw" if self.cur().value == "not" else "op").value
            expr = self.unary()
            return Unary(op, expr, self.cur().line)
        return self.postfix()

    def postfix(self) -> Any:
        expr = self.primary()
        while True:
            if self.match("("):
                args: List[Any] = []
                if not self.match(")"):
                    args.append(self.expr())
                    while self.match(","):
                        args.append(self.expr())
                    self.eat(")")
                expr = Call(expr, args, expr.line if hasattr(expr, "line") else self.cur().line)
            elif self.match("["):
                idx = self.expr()
                self.eat("]")
                expr = Index(expr, idx, expr.line if hasattr(expr, "line") else self.cur().line)
            elif self.match("."):
                name = self.eat("ident").value
                expr = Get(expr, name, expr.line if hasattr(expr, "line") else self.cur().line)
            else:
                break
        return expr

    def primary(self) -> Any:
        t = self.cur()
        if t.value == "absolute" or t.value == "true":
            self.eat("kw")
            return Literal(True, t.line)
        if t.value == "void" or t.value == "false":
            self.eat("kw")
            return Literal(False, t.line)
        if t.kind == "num":
            self.pos += 1
            val: Any = float(t.value) if "." in t.value else int(t.value)
            return Literal(val, t.line)
        if t.kind == "str":
            self.pos += 1
            inner = bytes(t.value[1:-1], "utf-8").decode("unicode_escape")
            return Literal(inner, t.line)
        if t.value == "[":
            self.pos += 1
            items: List[Any] = []
            if not self.match("]"):
                items.append(self.expr())
                while self.match(","):
                    items.append(self.expr())
                self.eat("]")
            return ListLit(items, t.line)
        if t.kind == "ident":
            self.pos += 1
            return Var(t.value, t.line)
        if t.value == "(":
            self.pos += 1
            e = self.expr()
            self.eat(")")
            return e
        raise cyn(f"Unexpected token '{t.value}'. The Solver does not recognize this directive.", t.line, t.col)


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


class BreakSignal(Exception):
    pass


class ContinueSignal(Exception):
    pass


class Interpreter:
    def __init__(self, base_path: str):
        self.base_path = base_path
        self.globals: Dict[str, Any] = self._builtins()
        self.loaded: set[str] = set()

    def _builtins(self) -> Dict[str, Any]:
        return {
            "spawn": self._builtin_spawn,
            "len": lambda x: len(x),
            "str": str,
            "num": float,
            "floor": math.floor,
            "ceil": math.ceil,
            "abs": abs,
            "min": min,
            "max": max,
            "Cyn": CYN,
            "Uzi": "Uzi Doorman",
            "N": "Serial Designation N",
            "J": "Serial Designation J",
            "V": "Serial Designation V",
            "Copper9": "Copper 9",
            "JCJenson": "JCJenson In Spaaace!",
        }

    def _builtin_spawn(self, *args: Any) -> None:
        print(*[self._stringify(a) for a in args])

    def _stringify(self, v: Any) -> str:
        if v is None or v is False and v is not True:
            if v is None:
                return "void"
        if isinstance(v, bool):
            return "absolute" if v else "void"
        if isinstance(v, float) and v.is_integer():
            return str(int(v))
        return str(v)

    def run_file(self, path: str) -> None:
        with open(path, "r", encoding="utf-8") as f:
            source = f.read()
        self.base_path = os.path.dirname(os.path.abspath(path))
        self.execute(source, path)

    def execute(self, source: str, path: str = "<stdin>") -> None:
        tokens = lex(source, path)
        prog = Parser(tokens, path).parse()
        self.run_program(prog)

    def run_program(self, prog: Program) -> None:
        for stmt in prog.stmts:
            self.eval_stmt(stmt, self.globals)

    def resolve_directive(self, path: str, line: int) -> None:
        if not path.endswith(".md"):
            path += ".md"
        full = os.path.normpath(os.path.join(self.base_path, path))
        if full in self.loaded:
            return
        if not os.path.isfile(full):
            raise cyn(f"Directive '{path}' not found — Cyn did not authorize this import.", line)
        self.loaded.add(full)
        with open(full, "r", encoding="utf-8") as f:
            self.execute(f.read(), full)

    def eval_stmt(self, stmt: Any, env: Dict[str, Any]) -> Any:
        if isinstance(stmt, Spawn):
            val = self.eval_expr(stmt.expr, env)
            print(self._stringify(val))
            return None
        if isinstance(stmt, Core):
            env[stmt.name] = self.eval_expr(stmt.expr, env)
            return None
        if isinstance(stmt, Assign):
            if stmt.name not in env:
                raise cyn(f"Unknown core '{stmt.name}' — register it first.", stmt.line)
            env[stmt.name] = self.eval_expr(stmt.expr, env)
            return None
        if isinstance(stmt, Drone):
            env[stmt.name] = self._make_drone(stmt, env)
            return None
        if isinstance(stmt, Hive):
            if self._truthy(self.eval_expr(stmt.cond, env)):
                return self.run_block(stmt.then, env)
            if stmt.alt:
                return self.run_block(stmt.alt, env)
            return None
        if isinstance(stmt, Reboot):
            while self._truthy(self.eval_expr(stmt.cond, env)):
                try:
                    self.run_block(stmt.body, env)
                except ContinueSignal:
                    continue
                except BreakSignal:
                    break
            return None
        if isinstance(stmt, Corrupt):
            start = int(self.eval_expr(stmt.start, env))
            end = int(self.eval_expr(stmt.end, env))
            step = 1 if start <= end else -1
            i = start
            while (step > 0 and i <= end) or (step < 0 and i >= end):
                env[stmt.var] = i
                try:
                    self.run_block(stmt.body, env)
                except ContinueSignal:
                    i += step
                    continue
                except BreakSignal:
                    break
                i += step
            return None
        if isinstance(stmt, Terminate):
            val = self.eval_expr(stmt.expr, env) if stmt.expr else None
            raise ReturnSignal(val)
        if isinstance(stmt, Ascend):
            raise BreakSignal()
        if isinstance(stmt, Reformat):
            raise ContinueSignal()
        if isinstance(stmt, Directive):
            self.resolve_directive(stmt.path, stmt.line)
            return None
        if isinstance(stmt, Block):
            return self.run_block(stmt, env)
        if isinstance(stmt, ExprStmt):
            return self.eval_expr(stmt.expr, env)
        return None

    def run_block(self, block: Block, env: Dict[str, Any]) -> Any:
        result = None
        for stmt in block.stmts:
            result = self.eval_stmt(stmt, env)
        return result

    def _make_drone(self, drone: Drone, closure: Dict[str, Any]) -> Callable:
        def fn(*args: Any) -> Any:
            local = dict(closure)
            if len(args) != len(drone.params):
                raise cyn(
                    f"Drone '{drone.name}' expects {len(drone.params)} units, got {len(args)}.",
                    drone.line,
                )
            for name, val in zip(drone.params, args):
                local[name] = val
            try:
                self.run_block(drone.body, local)
                return None
            except ReturnSignal as sig:
                return sig.value
        return fn

    def _truthy(self, v: Any) -> bool:
        if v is None or v is False:
            return False
        if isinstance(v, (int, float)) and v == 0:
            return False
        if isinstance(v, str) and v == "":
            return False
        if isinstance(v, list) and len(v) == 0:
            return False
        return True

    def eval_expr(self, expr: Any, env: Dict[str, Any]) -> Any:
        if isinstance(expr, Literal):
            return expr.value
        if isinstance(expr, Var):
            if expr.name not in env:
                raise cyn(f"Unknown unit '{expr.name}' — Cyn has no record of it.", expr.line)
            return env[expr.name]
        if isinstance(expr, ListLit):
            return [self.eval_expr(e, env) for e in expr.items]
        if isinstance(expr, Unary):
            v = self.eval_expr(expr.expr, env)
            if expr.op == "-":
                return -self._num(v, expr.line)
            if expr.op == "not":
                return not self._truthy(v)
            raise cyn(f"Unknown unary '{expr.op}'.", expr.line)
        if isinstance(expr, BinOp):
            l = self.eval_expr(expr.left, env)
            r = self.eval_expr(expr.right, env)
            if expr.op == "+":
                if isinstance(l, str) or isinstance(r, str):
                    return self._stringify(l) + self._stringify(r)
                return self._num(l, expr.line) + self._num(r, expr.line)
            if expr.op == "-":
                return self._num(l, expr.line) - self._num(r, expr.line)
            if expr.op == "*":
                return self._num(l, expr.line) * self._num(r, expr.line)
            if expr.op == "/":
                rn = self._num(r, expr.line)
                if rn == 0:
                    raise cyn("Division by void — Copper 9 math collapse.", expr.line)
                return self._num(l, expr.line) / rn
            if expr.op == "%":
                return int(self._num(l, expr.line)) % int(self._num(r, expr.line))
            if expr.op == "==":
                return l == r
            if expr.op == "!=":
                return l != r
            if expr.op == "<":
                return self._num(l, expr.line) < self._num(r, expr.line)
            if expr.op == ">":
                return self._num(l, expr.line) > self._num(r, expr.line)
            if expr.op == "<=":
                return self._num(l, expr.line) <= self._num(r, expr.line)
            if expr.op == ">=":
                return self._num(l, expr.line) >= self._num(r, expr.line)
            if expr.op == "and":
                return self._truthy(l) and self._truthy(r)
            if expr.op == "or":
                return self._truthy(l) or self._truthy(r)
            raise cyn(f"Unknown operator '{expr.op}'.", expr.line)
        if isinstance(expr, Call):
            callee = self.eval_expr(expr.callee, env)
            args = [self.eval_expr(a, env) for a in expr.args]
            if callable(callee):
                return callee(*args)
            raise cyn("That unit is not a drone — cannot invoke.", expr.line)
        if isinstance(expr, Index):
            obj = self.eval_expr(expr.obj, env)
            idx = int(self.eval_expr(expr.index, env))
            try:
                return obj[idx]
            except (IndexError, TypeError, KeyError):
                raise cyn("Index out of hive bounds.", expr.line)
        if isinstance(expr, Get):
            obj = self.eval_expr(expr.obj, env)
            name = expr.name
            if isinstance(obj, dict) and name in obj:
                return obj[name]
            raise cyn(f"Field '{name}' dissolved.", expr.line)
        raise cyn("Internal Solver fault.", 0)

    def _num(self, v: Any, line: int) -> float:
        if isinstance(v, (int, float)):
            return float(v)
        if isinstance(v, str):
            try:
                return float(v)
            except ValueError:
                pass
        raise cyn("Expected a number, got something else.", line)


def banner() -> None:
    print(f";; MD {VERSION} — Murder Drones language")
    print(f";; {CYN} · Absolute Solver · Copper 9 directive channel")
    print()


def main() -> int:
    parser = argparse.ArgumentParser(
        prog="md",
        description="MD language — Cyn is the boss.",
    )
    parser.add_argument("file", nargs="?", help=".md source file")
    parser.add_argument("-e", "--eval", help="run source string")
    parser.add_argument("-v", "--version", action="store_true", help="show version")
    parser.add_argument("-q", "--quiet", action="store_true", help="suppress Cyn banner")
    args = parser.parse_args()

    if args.version:
        print(f"md {VERSION} — overseen by {CYN}")
        return 0

    if not args.quiet:
        banner()

    interp = Interpreter(os.getcwd())
    try:
        if args.eval:
            interp.execute(args.eval)
        elif args.file:
            interp.run_file(args.file)
        else:
            print(f"[{CYN}] Awaiting directives. Pipe a .md file or use: md script.md")
            for line in sys.stdin:
                interp.execute(line)
        return 0
    except CynError as e:
        print(str(e), file=sys.stderr)
        return 1
    except KeyboardInterrupt:
        print(f"\n[{CYN}] Terminated by operator.", file=sys.stderr)
        return 130


if __name__ == "__main__":
    sys.exit(main())
