// Parser for the dashboard's Rhai mode.
//
// Recursive descent for statements, Pratt precedence climbing for
// expressions. Error-tolerant: on unexpected input the parser records an
// error, resyncs to the next `;` or matching `}`, and keeps going. The
// AST it returns is best-effort — partial trees are fine; callers
// (autocomplete, goto-def) tolerate gaps.

import type {
	BlockExpr,
	Expr,
	FnDecl,
	IfExpr,
	ObjectMapEntry,
	Param,
	ParseError,
	ParseResult,
	Stmt,
	SwitchArm
} from './ast';
import { tokenize, type Token, type TokenKind } from './lexer';

export function parse(source: string): ParseResult {
	const { tokens, comments, blankLines } = tokenize(source);
	const p = new Parser(source, tokens);
	const program = p.parseProgram();
	return { source, program, errors: p.errors, comments, blankLines };
}

// Precedence levels for binary operators. Higher binds tighter. Assignment
// is special-cased outside the binary chain because it's right-associative
// and only legal at the top of an expression.
const BINARY_PRECEDENCE: Record<string, number> = {
	'??': 1,
	'||': 2,
	'&&': 3,
	'==': 4,
	'!=': 4,
	'<': 5,
	'<=': 5,
	'>': 5,
	'>=': 5,
	'|': 6,
	'^': 7,
	'&': 8,
	'<<': 9,
	'>>': 9,
	'+': 10,
	'-': 10,
	'*': 11,
	'/': 11,
	'%': 11,
	'..': 12,
	'..=': 12
};

const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '??=']);
const UNARY_OPS = new Set(['!', '-', '+', '~']);

class Parser {
	pos = 0;
	errors: ParseError[] = [];
	constructor(
		private source: string,
		private tokens: Token[]
	) {}

	// -------------------------------------------------------------------- nav

	private peek(offset = 0): Token {
		return this.tokens[Math.min(this.pos + offset, this.tokens.length - 1)];
	}

	private advance(): Token {
		const t = this.tokens[this.pos];
		if (this.pos < this.tokens.length - 1) this.pos++;
		return t;
	}

	private match(kind: TokenKind, text?: string): boolean {
		const t = this.peek();
		if (t.kind !== kind) return false;
		if (text !== undefined && t.text !== text) return false;
		this.advance();
		return true;
	}

	private check(kind: TokenKind, text?: string): boolean {
		const t = this.peek();
		if (t.kind !== kind) return false;
		if (text !== undefined && t.text !== text) return false;
		return true;
	}

	// `role` is a human-readable description of what was expected, used
	// in place of the bare token kind so the message reads like Rhai's
	// own diagnostics (`Expecting name of a variable` rather than
	// `expected ident`). Falls back to the literal/kind when omitted.
	private expect(kind: TokenKind, text?: string, role?: string): Token {
		const t = this.peek();
		if (t.kind === kind && (text === undefined || t.text === text)) {
			return this.advance();
		}
		if (t.kind === 'EOF') {
			this.error(t, role ? `Expecting ${role} — script is incomplete` : 'Script is incomplete');
		} else {
			const desc = role ?? (text !== undefined ? `'${text}'` : kind.toLowerCase());
			this.error(t, `Expecting ${desc}`);
		}
		// Return the token without consuming so the caller's parent can
		// still resync at its own boundary.
		return t;
	}

	private error(at: Token, message: string): void {
		this.errors.push({ start: at.start, end: at.end, message });
	}

	// Resync to the next statement boundary inside the current block. Used
	// when a statement fails to parse — we drop tokens until we either land
	// on `;` (consumed) or `}` / EOF (left for the caller).
	private resyncStmt(): void {
		let depth = 0;
		while (true) {
			const t = this.peek();
			if (t.kind === 'EOF') return;
			if (t.kind === 'Punct') {
				if (t.text === '{' || t.text === '(' || t.text === '[') depth++;
				else if (t.text === '}' || t.text === ')' || t.text === ']') {
					if (depth === 0) return;
					depth--;
				} else if (depth === 0 && t.text === ';') {
					this.advance();
					return;
				}
			}
			this.advance();
		}
	}

	// ------------------------------------------------------------ top level

	parseProgram(): BlockExpr {
		const start = this.peek().start;
		const stmts: Stmt[] = [];
		while (this.peek().kind !== 'EOF') {
			const before = this.pos;
			const stmt = this.parseStmt();
			if (stmt) stmts.push(stmt);
			else if (this.pos === before) {
				// No forward progress — drop a token to avoid an infinite loop.
				this.advance();
			}
		}
		const last = this.tokens[this.tokens.length - 1];
		return { kind: 'BlockExpr', start, end: last.end, stmts };
	}

	// ----------------------------------------------------------- statements

	private parseStmt(): Stmt | null {
		const t = this.peek();

		if (t.kind === 'Keyword') {
			switch (t.text) {
				case 'let':
					return this.parseLetOrConst('Let');
				case 'const':
					return this.parseLetOrConst('Const');
				case 'fn':
					return this.parseFnDecl();
				case 'return':
					return this.parseReturn();
				case 'while':
					return this.parseWhile();
				case 'loop':
					return this.parseLoop();
				case 'for':
					return this.parseFor();
				case 'break': {
					this.advance();
					const semi = this.match('Punct', ';');
					return { kind: 'Break', start: t.start, end: semi ? t.end + 1 : t.end };
				}
				case 'continue': {
					this.advance();
					const semi = this.match('Punct', ';');
					return { kind: 'Continue', start: t.start, end: semi ? t.end + 1 : t.end };
				}
				case 'try':
					return this.parseTry();
			}
		}

		// Stray semicolons are no-ops; consume and try again.
		if (this.match('Punct', ';')) return null;

		// Expression statement (also covers if/switch/block-as-stmt because
		// those parse as expressions).
		const expr = this.tryParseExpr();
		if (!expr) {
			const bad = this.peek();
			this.error(bad, bad.kind === 'EOF' ? 'Script is incomplete' : `Unexpected token '${bad.text}'`);
			this.resyncStmt();
			return null;
		}
		const semi = this.match('Punct', ';');
		return {
			kind: 'ExprStmt',
			start: expr.start,
			end: semi ? this.tokens[this.pos - 1].end : expr.end,
			expr,
			semi
		};
	}

	private parseLetOrConst(kind: 'Let' | 'Const'): Stmt {
		const start = this.advance().start; // let|const
		const nameTok = this.expect('Ident', undefined, 'name of a variable');
		const name = nameTok.text;
		const nameRange = { start: nameTok.start, end: nameTok.end };
		let init: Expr | null = null;
		if (this.match('Operator', '=')) {
			init = this.tryParseExpr() ?? null;
		}
		const semi = this.match('Punct', ';');
		const end = semi ? this.tokens[this.pos - 1].end : init ? init.end : nameTok.end;
		return { kind, start, end, name, nameRange, init } as Stmt;
	}

	private parseFnDecl(): FnDecl {
		const start = this.advance().start; // fn
		const nameTok = this.expect('Ident', undefined, 'function name in function declaration');
		this.expect('Punct', '(');
		const params: Param[] = [];
		while (!this.check('Punct', ')') && this.peek().kind !== 'EOF') {
			const pTok = this.expect('Ident', undefined, 'parameter name');
			params.push({ name: pTok.text, start: pTok.start, end: pTok.end });
			if (!this.match('Punct', ',')) break;
		}
		this.expect('Punct', ')');
		const body = this.parseBlockExpr();
		return {
			kind: 'FnDecl',
			start,
			end: body.end,
			name: nameTok.text,
			nameRange: { start: nameTok.start, end: nameTok.end },
			params,
			body
		};
	}

	private parseReturn(): Stmt {
		const start = this.advance().start; // return
		let value: Expr | null = null;
		if (!this.check('Punct', ';') && !this.check('Punct', '}') && this.peek().kind !== 'EOF') {
			value = this.tryParseExpr() ?? null;
		}
		const semi = this.match('Punct', ';');
		const end = semi ? this.tokens[this.pos - 1].end : value ? value.end : start + 'return'.length;
		return { kind: 'Return', start, end, value };
	}

	private parseWhile(): Stmt {
		const start = this.advance().start; // while
		const cond = this.tryParseExpr() ?? this.placeholderExpr();
		const body = this.parseBlockExpr();
		return { kind: 'While', start, end: body.end, cond, body };
	}

	private parseLoop(): Stmt {
		const start = this.advance().start; // loop
		const body = this.parseBlockExpr();
		return { kind: 'Loop', start, end: body.end, body };
	}

	private parseFor(): Stmt {
		const start = this.advance().start; // for
		const nameTok = this.expect('Ident', undefined, 'loop variable name');
		this.expect('Keyword', 'in');
		const iter = this.tryParseExpr() ?? this.placeholderExpr();
		const body = this.parseBlockExpr();
		return {
			kind: 'For',
			start,
			end: body.end,
			varName: nameTok.text,
			varRange: { start: nameTok.start, end: nameTok.end },
			iter,
			body
		};
	}

	private parseTry(): Stmt {
		const start = this.advance().start; // try
		const body = this.parseBlockExpr();
		this.expect('Keyword', 'catch');
		let catchVar: string | null = null;
		let catchVarRange: { start: number; end: number } | null = null;
		if (this.match('Punct', '(')) {
			if (this.check('Ident')) {
				const id = this.advance();
				catchVar = id.text;
				catchVarRange = { start: id.start, end: id.end };
			}
			this.expect('Punct', ')');
		}
		const handler = this.parseBlockExpr();
		return { kind: 'Try', start, end: handler.end, body, catchVar, catchVarRange, handler };
	}

	private parseBlockExpr(): BlockExpr {
		const openTok = this.peek();
		if (!this.match('Punct', '{')) {
			this.error(openTok, "Expecting '{' to begin a block");
			return { kind: 'BlockExpr', start: openTok.start, end: openTok.start, stmts: [] };
		}
		const start = openTok.start;
		const stmts: Stmt[] = [];
		while (!this.check('Punct', '}') && this.peek().kind !== 'EOF') {
			const before = this.pos;
			const s = this.parseStmt();
			if (s) stmts.push(s);
			else if (this.pos === before) this.advance();
		}
		const closeTok = this.peek();
		this.match('Punct', '}');
		return { kind: 'BlockExpr', start, end: closeTok.end, stmts };
	}

	// ---------------------------------------------------------- expressions

	private tryParseExpr(): Expr | null {
		const t = this.peek();
		if (t.kind === 'EOF' || (t.kind === 'Punct' && (t.text === ';' || t.text === '}' || t.text === ')' || t.text === ']' || t.text === ','))) {
			return null;
		}
		return this.parseAssign();
	}

	private parseAssign(): Expr {
		const left = this.parseBinary(0);
		const t = this.peek();
		if (t.kind === 'Operator' && ASSIGN_OPS.has(t.text)) {
			this.advance();
			const right = this.parseAssign();
			return { kind: 'Assign', start: left.start, end: right.end, op: t.text, target: left, value: right };
		}
		return left;
	}

	private parseBinary(minPrec: number): Expr {
		let left = this.parseUnary();
		while (true) {
			const t = this.peek();
			if (t.kind !== 'Operator') break;
			const prec = BINARY_PRECEDENCE[t.text];
			if (prec === undefined || prec < minPrec) break;
			this.advance();
			const right = this.parseBinary(prec + 1);
			left = { kind: 'Binary', start: left.start, end: right.end, op: t.text, left, right };
		}
		return left;
	}

	private parseUnary(): Expr {
		const t = this.peek();
		if (t.kind === 'Operator' && UNARY_OPS.has(t.text)) {
			this.advance();
			const operand = this.parseUnary();
			return { kind: 'Unary', start: t.start, end: operand.end, op: t.text, operand };
		}
		return this.parsePostfix(this.parsePrimary());
	}

	private parsePostfix(initial: Expr): Expr {
		let expr = initial;
		while (true) {
			const t = this.peek();
			if (t.kind === 'Punct' && t.text === '.') {
				this.advance();
				const prop = this.expect('Ident', undefined, 'name of a property');
				expr = {
					kind: 'Member',
					start: expr.start,
					end: prop.end,
					object: expr,
					property: prop.text,
					propertyRange: { start: prop.start, end: prop.end }
				};
			} else if (t.kind === 'Punct' && t.text === '(') {
				this.advance();
				const args: Expr[] = [];
				while (!this.check('Punct', ')') && this.peek().kind !== 'EOF') {
					const a = this.tryParseExpr();
					if (!a) break;
					args.push(a);
					if (!this.match('Punct', ',')) break;
				}
				const close = this.peek();
				this.expect('Punct', ')');
				expr = { kind: 'Call', start: expr.start, end: close.end, callee: expr, args };
			} else if (t.kind === 'Punct' && t.text === '[') {
				this.advance();
				const idx = this.tryParseExpr() ?? this.placeholderExpr();
				const close = this.peek();
				this.expect('Punct', ']');
				expr = { kind: 'Index', start: expr.start, end: close.end, object: expr, index: idx };
			} else if (t.kind === 'Operator' && t.text === '::') {
				// Namespace path: treat `log::info` as a Member chain on an
				// Ident so completion and lookup can walk the same shape.
				this.advance();
				const next = this.expect('Ident', undefined, "name after '::'");
				expr = {
					kind: 'Member',
					start: expr.start,
					end: next.end,
					object: expr,
					property: next.text,
					propertyRange: { start: next.start, end: next.end }
				};
			} else {
				break;
			}
		}
		return expr;
	}

	private parsePrimary(): Expr {
		const t = this.peek();

		// Literals
		if (t.kind === 'Number') {
			this.advance();
			return { kind: 'Number', start: t.start, end: t.end, raw: t.text };
		}
		if (t.kind === 'String') {
			this.advance();
			const quote = t.text.charAt(0) === '`' ? '`' : '"';
			return { kind: 'String', start: t.start, end: t.end, quote, raw: t.text };
		}
		if (t.kind === 'Keyword') {
			if (t.text === 'true' || t.text === 'false') {
				this.advance();
				return { kind: 'Bool', start: t.start, end: t.end, value: t.text === 'true' };
			}
			if (t.text === 'null') {
				this.advance();
				return { kind: 'Null', start: t.start, end: t.end };
			}
			if (t.text === 'if') return this.parseIfExpr();
			if (t.text === 'switch') return this.parseSwitchExpr();
			if (t.text === 'fn') return this.parseFnExpr();
		}

		// Identifier
		if (t.kind === 'Ident') {
			this.advance();
			return { kind: 'Ident', start: t.start, end: t.end, name: t.text };
		}

		// Paren expression
		if (t.kind === 'Punct' && t.text === '(') {
			this.advance();
			const inner = this.tryParseExpr() ?? this.placeholderExpr();
			const close = this.peek();
			this.expect('Punct', ')');
			return { kind: 'Paren', start: t.start, end: close.end, expr: inner };
		}

		// Array literal
		if (t.kind === 'Punct' && t.text === '[') {
			return this.parseArray();
		}

		// Object-map literal: `#{`
		if (t.kind === 'Punct' && t.text === '#' && this.peek(1).kind === 'Punct' && this.peek(1).text === '{') {
			return this.parseObjectMap();
		}

		// Block expression `{ ... }`
		if (t.kind === 'Punct' && t.text === '{') {
			return this.parseBlockExpr();
		}

		this.error(t, t.kind === 'EOF' ? 'Script is incomplete' : `Unexpected token '${t.text}'`);
		// Consume one token so we make forward progress, then return a
		// placeholder so the surrounding parser keeps its shape.
		this.advance();
		return this.placeholderExpr(t);
	}

	private parseIfExpr(): IfExpr {
		const start = this.advance().start; // if
		const cond = this.tryParseExpr() ?? this.placeholderExpr();
		const thenB = this.parseBlockExpr();
		let else_: BlockExpr | IfExpr | null = null;
		if (this.match('Keyword', 'else')) {
			if (this.check('Keyword', 'if')) {
				else_ = this.parseIfExpr();
			} else {
				else_ = this.parseBlockExpr();
			}
		}
		const end = else_ ? else_.end : thenB.end;
		return { kind: 'IfExpr', start, end, cond, then: thenB, else_ };
	}

	private parseSwitchExpr(): Expr {
		const start = this.advance().start; // switch
		const subject = this.tryParseExpr() ?? this.placeholderExpr();
		this.expect('Punct', '{');
		const arms: SwitchArm[] = [];
		while (!this.check('Punct', '}') && this.peek().kind !== 'EOF') {
			const armStart = this.peek().start;
			let pattern: Expr | null;
			if (this.check('Operator', '_') || (this.peek().kind === 'Ident' && this.peek().text === '_')) {
				this.advance();
				pattern = null;
			} else {
				pattern = this.tryParseExpr() ?? this.placeholderExpr();
			}
			let guard: Expr | null = null;
			if (this.match('Keyword', 'if')) {
				guard = this.tryParseExpr() ?? this.placeholderExpr();
			}
			this.expect('Operator', '=>');
			const value = this.tryParseExpr() ?? this.placeholderExpr();
			arms.push({ start: armStart, end: value.end, pattern, guard, value });
			if (!this.match('Punct', ',')) break;
		}
		const close = this.peek();
		this.expect('Punct', '}');
		return { kind: 'SwitchExpr', start, end: close.end, subject, arms };
	}

	private parseFnExpr(): Expr {
		// `fn (params) { ... }` — anonymous function expression. Rare in
		// Rhai but legal; some scripts use it for callbacks.
		const start = this.advance().start; // fn
		this.expect('Punct', '(');
		const params: Param[] = [];
		while (!this.check('Punct', ')') && this.peek().kind !== 'EOF') {
			const pTok = this.expect('Ident', undefined, 'parameter name');
			params.push({ name: pTok.text, start: pTok.start, end: pTok.end });
			if (!this.match('Punct', ',')) break;
		}
		this.expect('Punct', ')');
		const body = this.parseBlockExpr();
		return { kind: 'FnExpr', start, end: body.end, params, body };
	}

	private parseArray(): Expr {
		const start = this.advance().start; // [
		const elements: Expr[] = [];
		while (!this.check('Punct', ']') && this.peek().kind !== 'EOF') {
			const e = this.tryParseExpr();
			if (!e) break;
			elements.push(e);
			if (!this.match('Punct', ',')) break;
		}
		const close = this.peek();
		this.expect('Punct', ']');
		return { kind: 'Array', start, end: close.end, elements };
	}

	private parseObjectMap(): Expr {
		const start = this.advance().start; // #
		this.advance(); // {
		const entries: ObjectMapEntry[] = [];
		while (!this.check('Punct', '}') && this.peek().kind !== 'EOF') {
			const k = this.peek();
			let key: string;
			let keyRange: { start: number; end: number };
			if (k.kind === 'Ident' || k.kind === 'Keyword') {
				this.advance();
				key = k.text;
				keyRange = { start: k.start, end: k.end };
			} else if (k.kind === 'String') {
				this.advance();
				// Strip surrounding quotes for the key name (best-effort —
				// we don't decode escape sequences; this is only used for
				// completion labels).
				key = k.text.length >= 2 ? k.text.slice(1, -1) : k.text;
				keyRange = { start: k.start, end: k.end };
			} else {
				this.error(k, 'Expecting name of a map key');
				break;
			}
			this.expect('Punct', ':');
			const value = this.tryParseExpr() ?? this.placeholderExpr();
			entries.push({ start: keyRange.start, end: value.end, key, keyRange, value });
			if (!this.match('Punct', ',')) break;
		}
		const close = this.peek();
		this.expect('Punct', '}');
		return { kind: 'ObjectMap', start, end: close.end, entries };
	}

	private placeholderExpr(at?: Token): Expr {
		const t = at ?? this.peek();
		return { kind: 'Ident', start: t.start, end: t.start, name: '' };
	}
}
