#include "cbm.h"
#include "arena.h"
#include "helpers.h"
#include "lang_specs.h"
#include "extract_unified.h"
#include "tree_sitter/api.h" // TSNode, ts_node_*
#include "foundation/constants.h"
#include "extract_node_stack.h"

enum { MAX_EXCEPTION_NAME_LEN = 100, LAST_IDX = 1 };
#include <stdint.h> // uint32_t
#include <string.h>
#include <ctype.h>

// Field name length for ts_node_child_by_field_name() calls.
#define FIELD_LEN_CONSTRUCTOR 11 // strlen("constructor")

// --- Throw/Raise extraction ---

// Detect whether a node is a throw node for the given spec.
//
// Kotlin special case: this grammar models `throw X(...)` as a `jump_expression`
// (the same node also covers return/break/continue), NOT a `throw_expression`,
// so the spec's throw_node_types ("throw_expression") never matches.  We treat a
// Kotlin `jump_expression` as a throw only when its first child is the `throw`
// keyword, which excludes return/break/continue without false positives.
static bool is_throw_node(TSNode node, const CBMLangSpec *spec) {
    if (cbm_kind_in_set(node, spec->throw_node_types)) {
        return true;
    }
    if (spec->language == CBM_LANG_KOTLIN && strcmp(ts_node_type(node), "jump_expression") == 0) {
        uint32_t nc = ts_node_child_count(node);
        if (nc > 0 && strcmp(ts_node_type(ts_node_child(node, 0)), "throw") == 0) {
            return true;
        }
    }
    return false;
}

// Resolve exception name from the first meaningful child of a throw/raise node.
static char *resolve_exception_name(CBMArena *a, TSNode throw_node, const char *source) {
    uint32_t nc = ts_node_child_count(throw_node);
    for (uint32_t i = 0; i < nc; i++) {
        TSNode child = ts_node_child(throw_node, i);
        const char *ck = ts_node_type(child);
        if (strcmp(ck, "raise") == 0 || strcmp(ck, "throw") == 0) {
            continue;
        }
        if (ck[0] == ';' || ck[0] == '(' || ck[0] == ')') {
            continue;
        }

        if (strcmp(ck, "call") == 0 || strcmp(ck, "call_expression") == 0 ||
            strcmp(ck, "new_expression") == 0 || strcmp(ck, "object_creation_expression") == 0 ||
            strcmp(ck, "instance_expression") == 0) {
            TSNode fn = ts_node_child_by_field_name(child, TS_FIELD("function"));
            if (ts_node_is_null(fn)) {
                fn = ts_node_child_by_field_name(child, "constructor", FIELD_LEN_CONSTRUCTOR);
            }
            if (ts_node_is_null(fn)) {
                fn = ts_node_child_by_field_name(child, TS_FIELD("type"));
            }
            if (ts_node_is_null(fn) && ts_node_named_child_count(child) > 0) {
                fn = ts_node_named_child(child, 0);
            }
            if (!ts_node_is_null(fn)) {
                return cbm_node_text(a, fn, source);
            }
        } else {
            return cbm_node_text(a, child, source);
        }
        break;
    }
    return NULL;
}

// Extract exception types from a Java-style throws clause.
static void extract_throws_clause(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
                                  const char *func_qn) {
    if (!spec->throws_clause_field || !spec->throws_clause_field[0]) {
        return;
    }
    const char *kind = ts_node_type(node);
    if (strcmp(kind, "method_declaration") != 0 && strcmp(kind, "constructor_declaration") != 0) {
        return;
    }
    TSNode throws_clause = ts_node_child_by_field_name(node, spec->throws_clause_field,
                                                       (uint32_t)strlen(spec->throws_clause_field));
    if (ts_node_is_null(throws_clause)) {
        return;
    }
    uint32_t nc = ts_node_child_count(throws_clause);
    for (uint32_t i = 0; i < nc; i++) {
        TSNode child = ts_node_child(throws_clause, i);
        const char *ck = ts_node_type(child);
        if (strcmp(ck, "type_identifier") == 0 || strcmp(ck, "identifier") == 0 ||
            strcmp(ck, "scoped_type_identifier") == 0) {
            char *exc = cbm_node_text(ctx->arena, child, ctx->source);
            if (exc && exc[0]) {
                CBMThrow thr = {.exception_name = exc, .enclosing_func_qn = func_qn};
                cbm_throws_push(&ctx->result->throws, ctx->arena, thr);
            }
        }
    }
}

// Process a single node for throw extraction (called from iterative walker).
static void process_throw_node(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
    if (is_throw_node(node, spec)) {
        char *exc_name = resolve_exception_name(ctx->arena, node, ctx->source);
        if (exc_name && exc_name[0]) {
            if (strlen(exc_name) > MAX_EXCEPTION_NAME_LEN) {
                exc_name[MAX_EXCEPTION_NAME_LEN] = '\0';
            }
            CBMThrow thr;
            thr.exception_name = exc_name;
            thr.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node);
            cbm_throws_push(&ctx->result->throws, ctx->arena, thr);
        }
    }

    extract_throws_clause(ctx, node, spec, cbm_enclosing_func_qn_cached(ctx, node));
}

// Iterative throw walker
static void walk_throws(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
    TSNodeStack stack;
    ts_nstack_init(&stack, ctx, CBM_SZ_512);
    ts_nstack_push(&stack, root);
    while (stack.count > 0) {
        TSNode node = ts_nstack_pop(&stack);
        process_throw_node(ctx, node, spec);
        uint32_t count = ts_node_child_count(node);
        for (int i = (int)count - LAST_IDX; i >= 0; i--) {
            ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
        }
    }
}

// --- Read/Write detection (iterative) ---

// Resolve an assignment LHS node to the bare name being written.  Handles
// common wrappers so WRITES resolve for more languages than a raw identifier:
//   - expression_list (Go `x = ...` desugars left to expression_list[x])
//   - index/subscript (`cache[k] = v` → write the base var `cache`)
//   - field/member/selector access (`self.total = ...`, `obj.Field = ...` →
//     write the trailing field name `total`/`Field`)
// Returns NULL if no simple write target can be determined.
// Sets *is_member_out when the written name is the field half of a
// member/selector LHS — the receiver is stripped from the returned name, so
// this out-param is the only surviving record of the selector shape (#1962).
static char *resolve_lhs_write_name(CBMExtractCtx *ctx, TSNode left, bool *is_member_out) {
    // Unwrap a single-element expression_list (Go).
    if (strcmp(ts_node_type(left), "expression_list") == 0) {
        if (ts_node_named_child_count(left) != 1) {
            return NULL; // multi-assign: ambiguous, skip
        }
        left = ts_node_named_child(left, 0);
    }
    const char *lk = ts_node_type(left);
    if (strcmp(lk, "identifier") == 0 || strcmp(lk, "simple_identifier") == 0) {
        return cbm_node_text(ctx->arena, left, ctx->source);
    }
    // Indexed write: write the base operand's identifier (`cache[k]` → cache).
    if (strcmp(lk, "index_expression") == 0 || strcmp(lk, "subscript_expression") == 0) {
        TSNode base = ts_node_child_by_field_name(left, TS_FIELD("operand"));
        if (ts_node_is_null(base)) {
            base = ts_node_child_by_field_name(left, TS_FIELD("object"));
        }
        if (ts_node_is_null(base) && ts_node_named_child_count(left) > 0) {
            base = ts_node_named_child(left, 0);
        }
        if (!ts_node_is_null(base)) {
            const char *bk = ts_node_type(base);
            if (strcmp(bk, "identifier") == 0 || strcmp(bk, "simple_identifier") == 0) {
                return cbm_node_text(ctx->arena, base, ctx->source);
            }
        }
        return NULL;
    }
    // Field/member write: write the trailing field name (`self.total` → total,
    // `obj.Field` → Field).  Covers Rust field_expression, C#/Java member access.
    if (strcmp(lk, "field_expression") == 0 || strcmp(lk, "member_access_expression") == 0 ||
        strcmp(lk, "field_access") == 0 || strcmp(lk, "selector_expression") == 0) {
        TSNode fld = ts_node_child_by_field_name(left, TS_FIELD("field"));
        if (ts_node_is_null(fld)) {
            fld = ts_node_child_by_field_name(left, TS_FIELD("name"));
        }
        if (!ts_node_is_null(fld)) {
            if (is_member_out) {
                *is_member_out = true;
            }
            return cbm_node_text(ctx->arena, fld, ctx->source);
        }
        return NULL;
    }
    return NULL;
}

// Resolve the write target of a node in an assignment_node_types set.  For a
// plain assignment the target is the "left" field (or first child).  For an
// increment/decrement unary expression (`x++`, `++x`, C#
// postfix_/prefix_unary_expression) there is no "left" field and the operand
// may sit on either side of the operator token, so scan named children for the
// first identifier / member-style operand.  Returns a null node when no simple
// target is found.
static TSNode resolve_write_lhs_node(TSNode node) {
    TSNode left = ts_node_child_by_field_name(node, TS_FIELD("left"));
    if (!ts_node_is_null(left)) {
        return left;
    }
    const char *nk = ts_node_type(node);
    if (strcmp(nk, "postfix_unary_expression") == 0 || strcmp(nk, "prefix_unary_expression") == 0 ||
        strcmp(nk, "update_expression") == 0) {
        // Only ++/-- mutate their operand. Other unary postfix/prefix forms
        // (C# null-forgiving `x!`, address-of `&x`, deref `*x`, logical `!x`)
        // READ the operand — never treat them as writes.
        bool is_incdec = false;
        uint32_t total = ts_node_child_count(node);
        for (uint32_t i = 0; i < total; i++) {
            TSNode c = ts_node_child(node, i);
            if (ts_node_is_named(c)) {
                continue; // operator is an anonymous token
            }
            const char *op = ts_node_type(c);
            if (strcmp(op, "++") == 0 || strcmp(op, "--") == 0) {
                is_incdec = true;
                break;
            }
        }
        if (!is_incdec) {
            return (TSNode){0};
        }
        uint32_t cnc = ts_node_named_child_count(node);
        for (uint32_t i = 0; i < cnc; i++) {
            TSNode c = ts_node_named_child(node, i);
            const char *ck = ts_node_type(c);
            if (strcmp(ck, "identifier") == 0 || strcmp(ck, "simple_identifier") == 0 ||
                strcmp(ck, "member_access_expression") == 0 ||
                strcmp(ck, "field_expression") == 0 || strcmp(ck, "field_access") == 0 ||
                strcmp(ck, "selector_expression") == 0 || strcmp(ck, "subscript_expression") == 0 ||
                strcmp(ck, "index_expression") == 0) {
                return c;
            }
        }
        return (TSNode){0};
    }
    if (ts_node_child_count(node) > 0) {
        return ts_node_child(node, 0);
    }
    return (TSNode){0};
}

// Try to emit a write for an assignment node.
static void try_emit_assignment_write(CBMExtractCtx *ctx, TSNode node, const char *func_qn) {
    TSNode left = resolve_write_lhs_node(node);
    if (ts_node_is_null(left)) {
        return;
    }
    bool is_member = false;
    char *name = resolve_lhs_write_name(ctx, left, &is_member);
    if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
        CBMReadWrite rw;
        rw.var_name = name;
        rw.is_write = true;
        rw.enclosing_func_qn = func_qn;
        rw.is_member_access = is_member;
        cbm_rw_push(&ctx->result->rw, ctx->arena, rw);
    }
}

static void walk_readwrites(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
    TSNodeStack stack;
    ts_nstack_init(&stack, ctx, CBM_SZ_512);
    ts_nstack_push(&stack, root);
    while (stack.count > 0) {
        TSNode node = ts_nstack_pop(&stack);
        if (cbm_kind_in_set(node, spec->assignment_node_types)) {
            try_emit_assignment_write(ctx, node, cbm_enclosing_func_qn_cached(ctx, node));
        }
        uint32_t count = ts_node_child_count(node);
        for (int i = (int)count - LAST_IDX; i >= 0; i--) {
            ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
        }
    }
}

void cbm_extract_semantic(CBMExtractCtx *ctx) {
    const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
    if (!spec) {
        return;
    }

    // Throws
    if ((spec->throw_node_types && spec->throw_node_types[0]) ||
        (spec->throws_clause_field && spec->throws_clause_field[0])) {
        walk_throws(ctx, ctx->root, spec);
    }

    // Reads/Writes
    if (spec->assignment_node_types && spec->assignment_node_types[0]) {
        walk_readwrites(ctx, ctx->root, spec);
    }
}

// --- Unified handlers ---

void handle_throws(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
    bool has_throws = spec->throw_node_types && spec->throw_node_types[0];
    bool has_clause = spec->throws_clause_field && spec->throws_clause_field[0];
    if (!has_throws && !has_clause) {
        return;
    }

    if (has_throws && is_throw_node(node, spec)) {
        char *exc_name = resolve_exception_name(ctx->arena, node, ctx->source);
        if (exc_name && exc_name[0]) {
            if (strlen(exc_name) > MAX_EXCEPTION_NAME_LEN) {
                exc_name[MAX_EXCEPTION_NAME_LEN] = '\0';
            }
            CBMThrow thr;
            thr.exception_name = exc_name;
            thr.enclosing_func_qn = state->enclosing_func_qn;
            cbm_throws_push(&ctx->result->throws, ctx->arena, thr);
        }
    }

    extract_throws_clause(ctx, node, spec, state->enclosing_func_qn);
}

void handle_readwrites(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
    if (!spec->assignment_node_types || !spec->assignment_node_types[0]) {
        return;
    }

    if (cbm_kind_in_set(node, spec->assignment_node_types)) {
        TSNode left = resolve_write_lhs_node(node);

        if (!ts_node_is_null(left)) {
            bool is_member = false;
            char *name = resolve_lhs_write_name(ctx, left, &is_member);
            if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
                CBMReadWrite rw;
                rw.var_name = name;
                rw.is_write = true;
                rw.enclosing_func_qn = state->enclosing_func_qn;
                rw.is_member_access = is_member;
                cbm_rw_push(&ctx->result->rw, ctx->arena, rw);
            }
        }
    }
}