<?php
if (!defined('ABSPATH')) exit;

require_once ACC_PATH . 'includes/helpers/acc-event-engine.php';

class ACC_Accounting_Controller {

    public static function register_routes(): void {
        $ns = 'acc/v1';

        // ── Chart of Accounts ──────────────────────────────────────────────
        register_rest_route($ns, '/accounting/accounts', [
            ['methods' => 'GET',  'callback' => [self::class, 'get_accounts'],  'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'POST', 'callback' => [self::class, 'create_account'],'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        register_rest_route($ns, '/accounting/accounts/(?P<id>\d+)', [
            ['methods' => 'PUT',    'callback' => [self::class, 'update_account'], 'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'DELETE', 'callback' => [self::class, 'delete_account'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        // جزئیاتِ یک حساب: آرتیکل‌ها + گردش/مانده + ابعاد + آخرین اسناد
        register_rest_route($ns, '/accounting/accounts/(?P<id>\d+)/detail', [
            'methods' => 'GET', 'callback' => [self::class, 'account_detail'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);
        // اتصالِ بُعد به حساب
        register_rest_route($ns, '/accounting/accounts/(?P<id>\d+)/dims', [
            'methods' => 'POST', 'callback' => [self::class, 'add_account_dim'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/account-dims/(?P<id>\d+)', [
            'methods' => 'DELETE', 'callback' => [self::class, 'delete_account_dim'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);

        // ── Rules (Event Definition) ───────────────────────────────────────
        register_rest_route($ns, '/accounting/rules', [
            ['methods' => 'GET',  'callback' => [self::class, 'get_rules'],   'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'POST', 'callback' => [self::class, 'create_rule'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        register_rest_route($ns, '/accounting/rules/(?P<id>\d+)', [
            ['methods' => 'PUT',    'callback' => [self::class, 'update_rule'], 'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'DELETE', 'callback' => [self::class, 'delete_rule'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);

        // ── Events / Documents ─────────────────────────────────────────────
        register_rest_route($ns, '/accounting/events', [
            ['methods' => 'GET',  'callback' => [self::class, 'get_events'],   'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'POST', 'callback' => [self::class, 'create_event'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        register_rest_route($ns, '/accounting/events/(?P<id>\d+)/confirm', [
            'methods' => 'POST', 'callback' => [self::class, 'confirm_event'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/events/(?P<id>\d+)/cancel', [
            'methods' => 'POST', 'callback' => [self::class, 'cancel_event'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);

        // ── Flows ──────────────────────────────────────────────────────────
        register_rest_route($ns, '/accounting/flows', [
            'methods' => 'GET', 'callback' => [self::class, 'get_flows'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);

        // ── Reports ────────────────────────────────────────────────────────
        register_rest_route($ns, '/accounting/reports/trial-balance', [
            'methods' => 'GET', 'callback' => [self::class, 'trial_balance'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/reports/income-statement', [
            'methods' => 'GET', 'callback' => [self::class, 'income_statement'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/reports/balance-sheet', [
            'methods' => 'GET', 'callback' => [self::class, 'balance_sheet'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/audit', [
            'methods' => 'GET', 'callback' => [self::class, 'audit_report'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/reports/ledger', [
            'methods' => 'GET', 'callback' => [self::class, 'ledger'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/reports/flow-journal', [
            'methods' => 'GET', 'callback' => [self::class, 'flow_journal'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);

        // ── Dimensions ────────────────────────────────────────────────────
        register_rest_route($ns, '/accounting/dimensions', [
            ['methods' => 'GET',  'callback' => [self::class, 'get_dimensions'],   'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'POST', 'callback' => [self::class, 'create_dimension'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        register_rest_route($ns, '/accounting/dimensions/(?P<id>\d+)', [
            ['methods' => 'PUT',    'callback' => [self::class, 'update_dimension'], 'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'DELETE', 'callback' => [self::class, 'delete_dimension'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        // مقادیرِ بُعد
        register_rest_route($ns, '/accounting/dimension-tree', [
            'methods' => 'GET', 'callback' => [self::class, 'dimension_tree'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/dimension-values', [
            ['methods' => 'GET',  'callback' => [self::class, 'get_dim_values'],    'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'POST', 'callback' => [self::class, 'create_dim_value'],  'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        register_rest_route($ns, '/accounting/dimension-values/(?P<id>\d+)', [
            ['methods' => 'PUT',    'callback' => [self::class, 'update_dim_value'], 'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'DELETE', 'callback' => [self::class, 'delete_dim_value'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);

        // ── Post Event to Ledger (SP call) ─────────────────────────────────
        register_rest_route($ns, '/accounting/events/(?P<id>\d+)/post', [
            'methods'             => 'POST',
            'callback'            => [self::class, 'post_event_to_ledger'],
            'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/events/(?P<id>\d+)/reverse', [
            'methods'             => 'POST',
            'callback'            => [self::class, 'reverse_event'],
            'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/install-core', [
            'methods'             => 'POST',
            'callback'            => [self::class, 'install_core'],
            'permission_callback' => 'acc_jwt_check_permission',
        ]);

        // ── Event Templates (versioned JSON) ───────────────────────────────
        register_rest_route($ns, '/accounting/templates', [
            ['methods' => 'GET',  'callback' => [self::class, 'get_templates'],   'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'POST', 'callback' => [self::class, 'create_template'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        register_rest_route($ns, '/accounting/templates/(?P<id>\d+)', [
            ['methods' => 'GET',    'callback' => [self::class, 'get_template'],   'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'PUT',    'callback' => [self::class, 'update_template'], 'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'DELETE', 'callback' => [self::class, 'delete_template'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);

        // ── Article Bank (بانک آرتیکل) ─────────────────────────────────────
        register_rest_route($ns, '/accounting/articles', [
            ['methods' => 'GET',  'callback' => [self::class, 'get_articles'],   'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'POST', 'callback' => [self::class, 'create_article'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        register_rest_route($ns, '/accounting/articles/extract', [
            ['methods' => 'POST', 'callback' => [self::class, 'extract_articles'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        register_rest_route($ns, '/accounting/articles/(?P<id>\d+)', [
            ['methods' => 'PUT',    'callback' => [self::class, 'update_article'], 'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'DELETE', 'callback' => [self::class, 'delete_article'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);

        // ── Suggestion engine (موتورِ پیشنهاد) ─────────────────────────────
        register_rest_route($ns, '/accounting/suggest', [
            'methods' => 'GET', 'callback' => [self::class, 'suggest_articles'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);
        register_rest_route($ns, '/accounting/event-types', [
            'methods' => 'GET', 'callback' => [self::class, 'get_event_types'], 'permission_callback' => 'acc_jwt_check_permission',
        ]);

        // ── Dimension Groups ───────────────────────────────────────────────
        register_rest_route($ns, '/accounting/dimension-groups', [
            ['methods' => 'GET',  'callback' => [self::class, 'get_dim_groups'],   'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'POST', 'callback' => [self::class, 'create_dim_group'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        register_rest_route($ns, '/accounting/dimension-groups/(?P<id>\d+)', [
            ['methods' => 'PUT',    'callback' => [self::class, 'update_dim_group'], 'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'DELETE', 'callback' => [self::class, 'delete_dim_group'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);

        // ── GL–Dimension mandatory rules ───────────────────────────────────
        register_rest_route($ns, '/accounting/gl-dim-rules', [
            ['methods' => 'GET',  'callback' => [self::class, 'get_gl_dim_rules'],   'permission_callback' => 'acc_jwt_check_permission'],
            ['methods' => 'POST', 'callback' => [self::class, 'create_gl_dim_rule'], 'permission_callback' => 'acc_jwt_check_permission'],
        ]);
        register_rest_route($ns, '/accounting/gl-dim-rules/(?P<id>\d+)', [
            'methods'             => 'DELETE',
            'callback'            => [self::class, 'delete_gl_dim_rule'],
            'permission_callback' => 'acc_jwt_check_permission',
        ]);

        // ── Accounting Graph ───────────────────────────────────────────────
        register_rest_route($ns, '/accounting/graph', [
            'methods'             => 'GET',
            'callback'            => [self::class, 'get_graph'],
            'permission_callback' => 'acc_jwt_check_permission',
        ]);

        // ── Schema migration (run v2 SQL) ──────────────────────────────────
        register_rest_route($ns, '/accounting/install-v2', [
            'methods'             => 'POST',
            'callback'            => [self::class, 'run_install_v2'],
            'permission_callback' => 'acc_jwt_check_permission',
        ]);
    }

    // =========================================================
    // ACCOUNTS
    // =========================================================

    public static function get_accounts(WP_REST_Request $req): array {
        global $wpdb;
        $t = $wpdb->prefix . 'ai_acc_accounts';

        $parent_raw = $req->get_param('parent_id');
        $flat       = (bool) $req->get_param('flat');

        // قرارداد واحدِ GL: شناسه = account_id (کلیدِ کسب‌وکاری که template‌ها به آن ارجاع می‌دهند).
        // در خروجی API به code/name/id نام‌مستعار می‌شوند تا فرانت‌اند بدون تغییر کار کند.
        // (account_id با backfill همیشه = id نگه داشته می‌شود؛ ر.ک. create_account)
        $cols = "account_id AS id,
                 account_code AS code,
                 account_name AS name,
                 account_type,
                 parent_id,
                 dimension_type,
                 is_system,
                 is_dynamic";

        if ($flat) {
            $rows = $wpdb->get_results("SELECT {$cols} FROM {$t} ORDER BY account_code ASC", ARRAY_A);
        } elseif ($parent_raw !== null && $parent_raw !== '') {
            $rows = $wpdb->get_results(
                $wpdb->prepare("SELECT {$cols} FROM {$t} WHERE parent_id = %d ORDER BY account_code ASC", (int) $parent_raw),
                ARRAY_A
            );
        } else {
            $rows = $wpdb->get_results("SELECT {$cols} FROM {$t} WHERE parent_id IS NULL ORDER BY account_code ASC", ARRAY_A);
        }

        // تعداد فرزندان هر حساب (parent_id به id ارجاع می‌دهد)
        $ids = array_column($rows ?: [], 'id');
        $child_counts = [];
        if (!empty($ids)) {
            $placeholders = implode(',', array_fill(0, count($ids), '%d'));
            $counts = $wpdb->get_results(
                $wpdb->prepare("SELECT parent_id, COUNT(*) AS cnt FROM {$t} WHERE parent_id IN ({$placeholders}) GROUP BY parent_id", $ids),
                ARRAY_A
            );
            foreach ($counts as $c) {
                $child_counts[(int)$c['parent_id']] = (int)$c['cnt'];
            }
        }

        foreach ($rows as &$row) {
            $row['child_count'] = $child_counts[(int)$row['id']] ?? 0;
        }

        return ['success' => true, 'data' => $rows ?: []];
    }

    public static function create_account(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $parent = $req->get_param('parent_id');
        // فرانت‌اند code/name می‌فرستد؛ account_code/account_name هم پشتیبانی می‌شود
        $code = sanitize_text_field($req->get_param('code') ?? $req->get_param('account_code') ?? '');
        $name = sanitize_text_field($req->get_param('name') ?? $req->get_param('account_name') ?? '');
        if ($code === '' || $name === '') {
            return new WP_REST_Response(['success' => false, 'message' => 'کد و نام حساب الزامی است'], 400);
        }
        $wpdb->insert($wpdb->prefix . 'ai_acc_accounts', [
            'parent_id'      => ($parent !== null && $parent !== '') ? (int) $parent : null,
            'account_code'   => $code,
            'account_name'   => $name,
            'account_type'   => sanitize_key($req->get_param('account_type') ?: 'asset'),
            'dimension_type' => sanitize_text_field($req->get_param('dimension_type') ?? 'NONE'),
            'is_system'      => 0,
            'is_dynamic'     => (int) ($req->get_param('is_dynamic') ?? 0),
        ]);
        if ($wpdb->last_error) {
            return new WP_REST_Response(['success' => false, 'message' => $wpdb->last_error], 500);
        }
        // account_id را = id کن تا کلیدِ GL همیشه پر و معتبر باشد
        $new = (int) $wpdb->insert_id;
        $wpdb->update($wpdb->prefix . 'ai_acc_accounts', ['account_id' => $new], ['id' => $new]);
        return new WP_REST_Response(['success' => true, 'id' => $new], 201);
    }

    public static function update_account(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $id = (int) $req->get_param('id');
        $data = [];
        $code = $req->get_param('code') ?? $req->get_param('account_code');
        $name = $req->get_param('name') ?? $req->get_param('account_name');
        if ($code !== null)                    $data['account_code']   = sanitize_text_field($code);
        if ($name !== null)                    $data['account_name']   = sanitize_text_field($name);
        if ($req->has_param('account_type'))   $data['account_type']   = sanitize_key($req->get_param('account_type'));
        if ($req->has_param('dimension_type')) $data['dimension_type'] = sanitize_text_field($req->get_param('dimension_type'));
        if (!empty($data)) $wpdb->update($wpdb->prefix . 'ai_acc_accounts', $data, ['account_id' => $id]);
        return new WP_REST_Response(['success' => true], 200);
    }

    public static function delete_account(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $id = (int) $req->get_param('id');
        $children = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$wpdb->prefix}ai_acc_accounts WHERE parent_id = %d", $id
        ));
        if ($children > 0) {
            return new WP_REST_Response(['success' => false, 'message' => 'حساب دارای زیرحساب است'], 400);
        }
        $wpdb->delete($wpdb->prefix . 'ai_acc_accounts', ['account_id' => $id]);
        return new WP_REST_Response(['success' => true], 200);
    }

    /**
     * جزئیاتِ کاملِ یک حساب (بر اساس account_id):
     *   اطلاعات + مسیرِ والد + گردش/ماندهٔ تأییدشده + آرتیکل‌هایی که این حساب در آن‌ها
     *   نقش دارد (مشتق از template‌های فعال) + قواعدِ بُعدِ الزامی + ۲۰ سندِ اخیر.
     */
    public static function account_detail(WP_REST_Request $req): array {
        global $wpdb;
        $pre = $wpdb->prefix;
        $aid = (int) $req->get_param('id');

        $acc = $wpdb->get_row($wpdb->prepare(
            "SELECT account_id AS id, account_code AS code, account_name AS name, account_type, parent_id
             FROM {$pre}ai_acc_accounts WHERE account_id = %d", $aid
        ), ARRAY_A);
        if (!$acc) return ['success' => false, 'message' => 'حساب یافت نشد'];

        // مسیرِ والد (parent_id به id فیزیکی ارجاع می‌دهد = account_id)
        $path = []; $pid = $acc['parent_id']; $guard = 0;
        while ($pid !== null && $guard++ < 20) {
            $p = $wpdb->get_row($wpdb->prepare(
                "SELECT account_id AS id, account_name AS name, parent_id FROM {$pre}ai_acc_accounts WHERE id = %d",
                (int) $pid
            ), ARRAY_A);
            if (!$p) break;
            array_unshift($path, ['id' => (int) $p['id'], 'name' => $p['name']]);
            $pid = $p['parent_id'];
        }

        // گردش و مانده (تأییدشده): from=بدهکار، to=بستانکار
        $bal = $wpdb->get_row($wpdb->prepare(
            "SELECT
                COALESCE(SUM(CASE WHEN from_account_id = %d THEN amount ELSE 0 END), 0) AS total_debit,
                COALESCE(SUM(CASE WHEN to_account_id   = %d THEN amount ELSE 0 END), 0) AS total_credit,
                COUNT(*) AS flow_count
             FROM {$pre}ai_acc_flows
             WHERE (from_account_id = %d OR to_account_id = %d) AND is_confirmed = 1",
            $aid, $aid, $aid, $aid
        ), ARRAY_A);
        $bal['net_balance'] = (float) $bal['total_debit'] - (float) $bal['total_credit'];

        // آرتیکل‌ها از template‌های فعال
        $tpls = $wpdb->get_results(
            "SELECT id, event_type, template_name, mapping_logic
             FROM {$pre}ai_acc_event_templates WHERE is_active = 1 ORDER BY event_type ASC",
            ARRAY_A
        );
        $articles = []; $cp_ids = [];
        foreach ($tpls as $t) {
            $logic = is_string($t['mapping_logic']) ? json_decode($t['mapping_logic'], true) : $t['mapping_logic'];
            $pairs = (is_array($logic) && !empty($logic['pairs'])) ? $logic['pairs'] : [];
            foreach ($pairs as $p) {
                $dr = (int) ($p['dr_gl_id'] ?? 0);
                $cr = (int) ($p['cr_gl_id'] ?? 0);
                if ($dr !== $aid && $cr !== $aid) continue;
                $role = ($dr === $aid) ? 'DR' : 'CR';
                $cp   = ($role === 'DR') ? $cr : $dr;
                $cp_ids[$cp] = true;
                $articles[] = [
                    'template_id'    => (int) $t['id'],
                    'event_type'     => $t['event_type'],
                    'template_name'  => $t['template_name'],
                    'role'           => $role,
                    'counterpart_id' => $cp,
                    'source_field'   => $p['source_field'] ?? null,
                    'desc'           => $p['desc'] ?? ($p['description'] ?? ''),
                ];
            }
        }
        // نامِ حساب‌های مقابل
        $cp_map = [];
        if ($cp_ids) {
            $ids = array_keys($cp_ids);
            $ph  = implode(',', array_fill(0, count($ids), '%d'));
            $rows = $wpdb->get_results($wpdb->prepare(
                "SELECT account_id AS id, account_code AS code, account_name AS name
                 FROM {$pre}ai_acc_accounts WHERE account_id IN ({$ph})", $ids
            ), ARRAY_A);
            foreach ($rows as $r) $cp_map[(int) $r['id']] = $r;
        }
        foreach ($articles as &$a) {
            $c = $cp_map[$a['counterpart_id']] ?? null;
            $a['counterpart'] = $c
                ? ['id' => (int) $c['id'], 'code' => $c['code'], 'name' => $c['name']]
                : ['id' => $a['counterpart_id'], 'code' => '', 'name' => '#' . $a['counterpart_id']];
        }
        unset($a);

        // بُعدهای متصل به این حساب (account ↔ dimension)
        $account_dims = $wpdb->get_results($wpdb->prepare(
            "SELECT ad.id, ad.dim_id, ad.is_required, d.dim_code, d.dim_name, g.group_name
             FROM {$pre}ai_acc_account_dims ad
             JOIN {$pre}ai_acc_dimensions d ON d.id = ad.dim_id
             LEFT JOIN {$pre}ai_acc_dimension_groups g ON g.id = d.group_id
             WHERE ad.account_id = %d ORDER BY d.dim_code ASC", $aid
        ), ARRAY_A);

        // قواعدِ بُعدِ الزامی در سطحِ گروه (legacy gl_dim_rules)
        $dim_rules = $wpdb->get_results($wpdb->prepare(
            "SELECT g.group_code, g.group_name, r.is_required
             FROM {$pre}ai_acc_gl_dim_rules r
             JOIN {$pre}ai_acc_dimension_groups g ON g.id = r.group_id
             WHERE r.gl_id = %d ORDER BY g.sort_order ASC", $aid
        ), ARRAY_A);

        // ۲۰ سندِ اخیر
        $recent = $wpdb->get_results($wpdb->prepare(
            "SELECT f.id AS flow_id, e.id AS event_id, e.event_type,
                    CASE WHEN f.to_account_id = %d THEN 'in' ELSE 'out' END AS direction,
                    f.amount, f.description, f.is_confirmed, f.created_at
             FROM {$pre}ai_acc_flows f
             JOIN {$pre}ai_acc_events e ON e.id = f.event_id
             WHERE f.from_account_id = %d OR f.to_account_id = %d
             ORDER BY f.id DESC LIMIT 20", $aid, $aid, $aid
        ), ARRAY_A);

        return ['success' => true, 'data' => [
            'account'      => $acc,
            'path'         => $path,
            'balance'      => $bal,
            'articles'     => $articles,
            'account_dims' => $account_dims ?: [],
            'dim_rules'    => $dim_rules ?: [],
            'recent_flows' => $recent ?: [],
        ]];
    }

    // ── اتصالِ بُعد به حساب (account ↔ dimension) ──
    public static function add_account_dim(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $account_id = (int) $req->get_param('id');
        $dim_id     = (int) $req->get_param('dim_id');
        if (!$account_id || !$dim_id) {
            return new WP_REST_Response(['success' => false, 'message' => 'حساب و بُعد الزامی‌اند'], 400);
        }
        $wpdb->query($wpdb->prepare(
            "INSERT IGNORE INTO {$pre}ai_acc_account_dims (account_id, dim_id, is_required) VALUES (%d, %d, %d)",
            $account_id, $dim_id, (int) ($req->get_param('is_required') ?? 1)
        ));
        if ($wpdb->last_error) {
            return new WP_REST_Response(['success' => false, 'message' => $wpdb->last_error], 500);
        }
        return new WP_REST_Response(['success' => true, 'id' => $wpdb->insert_id], 201);
    }

    public static function delete_account_dim(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $wpdb->delete($wpdb->prefix . 'ai_acc_account_dims', ['id' => (int) $req->get_param('id')]);
        return new WP_REST_Response(['success' => true], 200);
    }

    // =========================================================
    // RULES
    // =========================================================

    public static function get_rules(WP_REST_Request $req): array {
        global $wpdb;
        $event_type = sanitize_text_field($req->get_param('event_type') ?? '');
        $where = $event_type ? $wpdb->prepare('WHERE r.event_type = %s', $event_type) : '';

        $rows = $wpdb->get_results(
            "SELECT r.*,
                    a.account_code, a.account_name,
                    sa.account_code AS source_code, sa.account_name AS source_name,
                    ta.account_code AS target_code, ta.account_name AS target_name
             FROM {$wpdb->prefix}ai_acc_rules r
             LEFT JOIN {$wpdb->prefix}ai_acc_accounts a  ON a.account_id  = r.account_id
             LEFT JOIN {$wpdb->prefix}ai_acc_accounts sa ON sa.account_id = r.source_account_id
             LEFT JOIN {$wpdb->prefix}ai_acc_accounts ta ON ta.account_id = r.target_account_id
             {$where}
             ORDER BY r.event_type ASC, r.priority ASC",
            ARRAY_A
        );
        return ['success' => true, 'data' => $rows ?: [], 'deprecated' => 'از Event Templates استفاده کنید؛ rules فقط fallback در post است'];
    }

    public static function create_rule(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $logic = $req->get_param('dynamic_target_logic');
        $wpdb->insert($wpdb->prefix . 'ai_acc_rules', [
            'event_type'             => sanitize_text_field($req->get_param('event_type')),
            'flow_direction'         => strtoupper(sanitize_key($req->get_param('flow_direction'))),
            'account_id'             => (int) $req->get_param('account_id'),
            'is_active'              => 1,
            'is_dimension_dependent' => (int) ($req->get_param('is_dimension_dependent') ?? 0),
            'priority'               => (int) ($req->get_param('priority') ?? 10),
            'source_account_id'      => $req->get_param('source_account_id') ? (int) $req->get_param('source_account_id') : null,
            'target_account_id'      => $req->get_param('target_account_id') ? (int) $req->get_param('target_account_id') : null,
            'dynamic_target_logic'   => is_string($logic) ? $logic : wp_json_encode($logic ?? ['type' => 'default']),
        ]);
        return new WP_REST_Response(['success' => true, 'id' => $wpdb->insert_id], 201);
    }

    public static function update_rule(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $id = (int) $req->get_param('id');
        $data = [];
        if ($req->has_param('is_active')) $data['is_active'] = (int) $req->get_param('is_active');
        if ($req->has_param('priority'))  $data['priority']  = (int) $req->get_param('priority');
        if ($req->has_param('dynamic_target_logic')) {
            $l = $req->get_param('dynamic_target_logic');
            $data['dynamic_target_logic'] = is_string($l) ? $l : wp_json_encode($l);
        }
        if (!empty($data)) $wpdb->update($wpdb->prefix . 'ai_acc_rules', $data, ['rule_id' => $id]);
        return new WP_REST_Response(['success' => true], 200);
    }

    public static function delete_rule(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $wpdb->delete($wpdb->prefix . 'ai_acc_rules', ['rule_id' => (int) $req->get_param('id')]);
        return new WP_REST_Response(['success' => true], 200);
    }

    // =========================================================
    // EVENTS / DOCUMENTS
    // =========================================================

    public static function get_events(WP_REST_Request $req): array {
        global $wpdb;
        $page   = max(1, (int) ($req->get_param('page') ?? 1));
        // فرانت‌اند per_page می‌فرستد؛ size هم پشتیبانی می‌شود
        $size   = max(1, min(100, (int) ($req->get_param('per_page') ?? $req->get_param('size') ?? 20)));
        $offset = ($page - 1) * $size;
        // workflow_state اکنون lowercase است؛ بدون تبدیل به uppercase
        $state  = sanitize_key($req->get_param('state') ?? '');
        $where  = $state ? $wpdb->prepare('WHERE e.workflow_state = %s', $state) : '';

        $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}ai_acc_events e {$where}");
        $rows  = $wpdb->get_results($wpdb->prepare(
            "SELECT e.*,
                    (SELECT COUNT(*) FROM {$wpdb->prefix}ai_acc_flows f WHERE f.event_id = e.id) AS flow_count
             FROM {$wpdb->prefix}ai_acc_events e
             {$where}
             ORDER BY e.id DESC
             LIMIT %d OFFSET %d",
            $size, $offset
        ), ARRAY_A);

        return ['success' => true, 'data' => $rows ?: [], 'total' => $total, 'page' => $page, 'size' => $size];
    }

    public static function create_event(WP_REST_Request $req): WP_REST_Response {
        $meta = $req->get_param('metadata');
        $payload = is_array($meta) ? $meta : (is_string($meta) ? (json_decode($meta, true) ?: []) : []);

        $result = Acc_Event_Engine::create_event([
            'event_type'       => sanitize_text_field($req->get_param('event_type')),
            'amount'           => (float) ($req->get_param('amount') ?? 0),
            'payload'          => $payload,
            'dimension_id'     => $req->get_param('dimension_id') ? (int) $req->get_param('dimension_id') : null,
            'description'      => sanitize_text_field($req->get_param('description') ?? ''),
            'idempotency_key'  => sanitize_text_field($req->get_param('idempotency_key') ?? ''),
            'reference_type'   => sanitize_text_field($req->get_param('reference_type') ?? ''),
            'reference_id'     => (int) ($req->get_param('reference_id') ?? 0),
            'source_module'    => sanitize_text_field($req->get_param('source_module') ?? 'erp_ui'),
        ]);

        $http = (int) ($result['http'] ?? ($result['success'] ? 201 : 500));
        unset($result['http']);
        if ($result['success']) {
            $result['data'] = ['id' => $result['event_id']];
        }
        return new WP_REST_Response($result, $http);
    }

    public static function reverse_event(WP_REST_Request $req): WP_REST_Response {
        $id = (int) $req->get_param('id');
        $result = Acc_Event_Engine::create_reversal($id, (int) get_current_user_id());
        $http = (int) ($result['http'] ?? ($result['success'] ? 200 : 400));
        unset($result['http']);
        return new WP_REST_Response($result, $http);
    }

    public static function install_core(WP_REST_Request $req): WP_REST_Response {
        if (!function_exists('acl_user_is_super_admin') || !acl_user_is_super_admin((int) get_current_user_id())) {
            return new WP_REST_Response(['success' => false, 'message' => 'فقط مدیر کل'], 403);
        }
        require_once ACC_PATH . 'includes/helpers/acc-schema.php';
        Acc_Schema::ensure_core_tables();
        update_option('acc_engine_db_version', '19.0');
        return new WP_REST_Response(['success' => true, 'db_version' => '19.0'], 200);
    }

    /**
     * تأیید نهایی: جریان‌های پیش‌نویسِ رویدادِ پست‌شده را قطعی می‌کند.
     * (append-only: چیزی پاک نمی‌شود، فقط is_confirmed=1)
     */
    public static function confirm_event(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $id    = (int) $req->get_param('id');
        $state = $wpdb->get_var($wpdb->prepare(
            "SELECT workflow_state FROM {$wpdb->prefix}ai_acc_events WHERE id = %d", $id
        ));
        if ($state === null) {
            return new WP_REST_Response(['success' => false, 'message' => 'رویداد یافت نشد'], 404);
        }
        if ($state !== 'posted') {
            return new WP_REST_Response(['success' => false, 'message' => 'فقط رویدادِ پست‌شده قابل تأیید است (وضعیت فعلی: ' . $state . ')'], 400);
        }
        $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}ai_acc_flows  SET is_confirmed = 1 WHERE event_id = %d", $id));
        $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}ai_acc_events SET workflow_state = 'confirmed', status = 'POSTED' WHERE id = %d", $id));
        return new WP_REST_Response(['success' => true], 200);
    }

    /**
     * لغو: فقط رویدادِ پست‌شده ولی تأییدنشده. جریان‌های پیش‌نویسش حذف می‌شوند.
     * (رویدادِ تأییدشده باید با سند برگشتی خنثی شود — در فاز بعد)
     */
    public static function cancel_event(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $id    = (int) $req->get_param('id');
        $state = $wpdb->get_var($wpdb->prepare(
            "SELECT workflow_state FROM {$wpdb->prefix}ai_acc_events WHERE id = %d", $id
        ));
        if ($state === null) {
            return new WP_REST_Response(['success' => false, 'message' => 'رویداد یافت نشد'], 404);
        }
        if ($state === 'confirmed') {
            return new WP_REST_Response(['success' => false, 'message' => 'رویدادِ تأییدشده قابل لغو نیست؛ باید سند برگشتی صادر شود'], 400);
        }
        $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}ai_acc_flows WHERE event_id = %d AND is_confirmed = 0", $id));
        $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}ai_acc_events SET workflow_state = 'cancelled' WHERE id = %d", $id));
        return new WP_REST_Response(['success' => true], 200);
    }

    // =========================================================
    // FLOWS
    // =========================================================

    public static function get_flows(WP_REST_Request $req): array {
        global $wpdb;
        $event_id = (int) ($req->get_param('event_id') ?? 0);
        if (!$event_id) return ['success' => false, 'message' => 'event_id الزامی است'];

        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT f.id AS flow_id, f.event_id, f.from_account_id, f.to_account_id, f.amount,
                    f.description, f.is_confirmed, f.entry_pair_id, f.line_no, f.created_at,
                    fa.account_code AS from_code, fa.account_name AS from_account_name,
                    ta.account_code AS to_code,   ta.account_name AS to_account_name
             FROM {$wpdb->prefix}ai_acc_flows f
             LEFT JOIN {$wpdb->prefix}ai_acc_accounts fa ON fa.account_id = f.from_account_id
             LEFT JOIN {$wpdb->prefix}ai_acc_accounts ta ON ta.account_id = f.to_account_id
             WHERE f.event_id = %d
             ORDER BY f.line_no ASC, f.id ASC",
            $event_id
        ), ARRAY_A);

        return ['success' => true, 'data' => $rows ?: []];
    }

    // =========================================================
    // REPORTS
    // =========================================================

    /**
     * ماندهٔ همهٔ حساب‌های دارای گردش (تأییدشده) با فیلترِ تاریخ.
     *   total_debit  = Σ مبالغی که حساب در نقشِ from (بدهکار) داشته
     *   total_credit = Σ مبالغی که حساب در نقشِ to   (بستانکار) داشته
     */
    private static function _account_balances(?string $from, ?string $to): array {
        global $wpdb; $pre = $wpdb->prefix;
        $conds = ['f.is_confirmed = 1']; $args = [];
        if ($from) { $conds[] = 'f.created_at >= %s'; $args[] = $from . ' 00:00:00'; }
        if ($to)   { $conds[] = 'f.created_at <= %s'; $args[] = $to   . ' 23:59:59'; }
        $fwhere = implode(' AND ', $conds);

        $sql = "SELECT a.account_id AS id, a.account_code AS code, a.account_name AS name, a.account_type,
                       COALESCE(SUM(CASE WHEN f.from_account_id = a.account_id THEN f.amount ELSE 0 END), 0) AS total_debit,
                       COALESCE(SUM(CASE WHEN f.to_account_id   = a.account_id THEN f.amount ELSE 0 END), 0) AS total_credit
                FROM {$pre}ai_acc_accounts a
                LEFT JOIN {$pre}ai_acc_flows f
                       ON (f.from_account_id = a.account_id OR f.to_account_id = a.account_id) AND {$fwhere}
                GROUP BY a.account_id, a.account_code, a.account_name, a.account_type
                HAVING total_debit <> 0 OR total_credit <> 0
                ORDER BY a.account_code ASC";
        $rows = $args ? $wpdb->get_results($wpdb->prepare($sql, ...$args), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);
        return $rows ?: [];
    }

    public static function trial_balance(WP_REST_Request $req): array {
        $from = $req->get_param('date_from') ?: null;
        $to   = $req->get_param('date_to')   ?: null;
        $rows = self::_account_balances($from ? sanitize_text_field($from) : null, $to ? sanitize_text_field($to) : null);
        foreach ($rows as &$r) {
            $r['net_balance'] = (float) $r['total_debit'] - (float) $r['total_credit'];
        }
        return ['success' => true, 'data' => $rows];
    }

    /** صورتِ سود و زیان: درآمد − کسورات − هزینه */
    public static function income_statement(WP_REST_Request $req): array {
        $from = $req->get_param('date_from') ?: null;
        $to   = $req->get_param('date_to')   ?: null;
        $rows = self::_account_balances($from ? sanitize_text_field($from) : null, $to ? sanitize_text_field($to) : null);

        $revenue = []; $contra = []; $expense = [];
        $rev_t = 0.0; $con_t = 0.0; $exp_t = 0.0;
        foreach ($rows as $r) {
            $dr = (float) $r['total_debit']; $cr = (float) $r['total_credit'];
            $line = ['code' => $r['code'], 'name' => $r['name']];
            switch ($r['account_type']) {
                case 'revenue': $line['amount'] = $cr - $dr; $revenue[] = $line; $rev_t += $line['amount']; break;
                case 'contra':  $line['amount'] = $dr - $cr; $contra[]  = $line; $con_t += $line['amount']; break;
                case 'expense': $line['amount'] = $dr - $cr; $expense[] = $line; $exp_t += $line['amount']; break;
            }
        }
        $net_sales  = $rev_t - $con_t;
        $net_income = $net_sales - $exp_t;

        return ['success' => true, 'data' => [
            'revenue' => $revenue, 'revenue_total' => $rev_t,
            'contra'  => $contra,  'contra_total'  => $con_t,
            'net_sales' => $net_sales,
            'expense' => $expense, 'expense_total' => $exp_t,
            'net_income' => $net_income,
        ]];
    }

    /** ترازنامه: دارایی = بدهی + حقوق + سود/زیانِ دوره */
    public static function balance_sheet(WP_REST_Request $req): array {
        $to   = $req->get_param('date_to') ?: ($req->get_param('as_of') ?: null);
        $rows = self::_account_balances(null, $to ? sanitize_text_field($to) : null);

        $asset = []; $liability = []; $equity = [];
        $as_t = 0.0; $li_t = 0.0; $eq_t = 0.0;
        $rev_t = 0.0; $con_t = 0.0; $exp_t = 0.0;
        foreach ($rows as $r) {
            $dr = (float) $r['total_debit']; $cr = (float) $r['total_credit'];
            $line = ['code' => $r['code'], 'name' => $r['name']];
            switch ($r['account_type']) {
                case 'asset':     $line['amount'] = $dr - $cr; $asset[]     = $line; $as_t += $line['amount']; break;
                case 'liability': $line['amount'] = $cr - $dr; $liability[] = $line; $li_t += $line['amount']; break;
                case 'equity':    $line['amount'] = $cr - $dr; $equity[]    = $line; $eq_t += $line['amount']; break;
                case 'revenue':   $rev_t += ($cr - $dr); break;
                case 'contra':    $con_t += ($dr - $cr); break;
                case 'expense':   $exp_t += ($dr - $cr); break;
            }
        }
        $net_income = ($rev_t - $con_t) - $exp_t;
        $liab_equity = $li_t + $eq_t + $net_income;
        $diff = round($as_t - $liab_equity, 2);

        return ['success' => true, 'data' => [
            'asset' => $asset, 'asset_total' => $as_t,
            'liability' => $liability, 'liability_total' => $li_t,
            'equity' => $equity, 'equity_total' => $eq_t,
            'net_income' => $net_income,
            'liab_equity_total' => $liab_equity,
            'balanced' => abs($diff) < 0.01,
            'difference' => $diff,
        ]];
    }

    // =========================================================
    // CONTINUOUS AUDIT  (حسابرسیِ پیوسته)
    // =========================================================

    /**
     * مجموعه‌ای از کنترل‌های حسابرسیِ پیوسته که به‌صورتِ کوئریِ زنده اجرا می‌شوند.
     * هر کنترل: { key, title, severity(ok|warn|fail), count, summary, headers[], rows[] }
     */
    public static function audit_report(WP_REST_Request $req): array {
        global $wpdb; $pre = $wpdb->prefix;
        $LIM = 25;

        // نگاشتِ نامِ حساب
        $accs = $wpdb->get_results("SELECT account_id, account_code, account_name FROM {$pre}ai_acc_accounts", ARRAY_A);
        $amap = [];
        foreach ($accs as $a) $amap[(int) $a['account_id']] = trim($a['account_code'] . ' ' . $a['account_name']);
        $nm = function ($id) use ($amap) { return $amap[(int) $id] ?? ('#' . (int) $id); };
        $fmt = function ($v) { return number_format((float) $v); };

        $controls = [];

        // ── A) انطباق با بانک آرتیکل (جفتِ حساب در بانک هست؟) ──
        $art_pairs = $wpdb->get_results("SELECT DISTINCT dr_account_id, cr_account_id FROM {$pre}ai_acc_articles WHERE is_active = 1", ARRAY_A);
        $bank = [];
        foreach ($art_pairs as $p) $bank[$p['dr_account_id'] . '|' . $p['cr_account_id']] = true;

        $groups = $wpdb->get_results(
            "SELECT from_account_id, to_account_id, COUNT(*) AS cnt, SUM(amount) AS total
             FROM {$pre}ai_acc_flows WHERE is_confirmed = 1
             GROUP BY from_account_id, to_account_id ORDER BY cnt DESC", ARRAY_A
        );
        $off_rows = []; $off_count = 0;
        foreach ($groups as $g) {
            if (isset($bank[$g['from_account_id'] . '|' . $g['to_account_id']])) continue;
            $off_count += (int) $g['cnt'];
            if (count($off_rows) < $LIM) {
                $off_rows[] = [$nm($g['from_account_id']) . ' → ' . $nm($g['to_account_id']), (int) $g['cnt'], $fmt($g['total'])];
            }
        }
        $controls[] = [
            'key' => 'conformance', 'title' => '🔗 انطباق با بانک آرتیکل',
            'severity' => $off_count ? 'warn' : 'ok', 'count' => $off_count,
            'summary' => $off_count ? "{$off_count} آرتیکلِ ثبت‌شده خارج از بانک آرتیکل" : 'همهٔ آرتیکل‌ها از بانک‌اند',
            'headers' => ['جفت حساب (بدهکار → بستانکار)', 'تعداد', 'مبلغ کل'], 'rows' => $off_rows,
        ];

        // ── B) نقصِ بُعدِ الزامی ──
        $dim_count = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$pre}ai_acc_flows f
             JOIN {$pre}ai_acc_gl_dim_rules r ON r.gl_id = f.from_account_id AND r.is_required = 1
             WHERE f.is_confirmed = 1 AND (f.dimension_id IS NULL OR f.dimension_id = 0)"
        );
        $dim_rows = [];
        if ($dim_count) {
            $rows = $wpdb->get_results(
                "SELECT f.event_id, e.event_type, f.from_account_id, f.amount
                 FROM {$pre}ai_acc_flows f
                 JOIN {$pre}ai_acc_gl_dim_rules r ON r.gl_id = f.from_account_id AND r.is_required = 1
                 JOIN {$pre}ai_acc_events e ON e.id = f.event_id
                 WHERE f.is_confirmed = 1 AND (f.dimension_id IS NULL OR f.dimension_id = 0)
                 ORDER BY f.id DESC LIMIT {$LIM}", ARRAY_A
            );
            foreach ($rows as $r) $dim_rows[] = ['#' . $r['event_id'], $r['event_type'], $nm($r['from_account_id']), $fmt($r['amount'])];
        }
        $controls[] = [
            'key' => 'dimensions', 'title' => '🏷️ نقصِ بُعدِ الزامی',
            'severity' => $dim_count ? 'fail' : 'ok', 'count' => $dim_count,
            'summary' => $dim_count ? "{$dim_count} آرتیکل روی حسابِ بُعد-اجباری بدونِ بُعد ثبت شده" : 'نقصِ بُعدی نیست',
            'headers' => ['سند', 'نوع رویداد', 'حساب بدهکار', 'مبلغ'], 'rows' => $dim_rows,
        ];

        // ── C) سلامتِ وضعیتِ اسناد ──
        $states = $wpdb->get_results("SELECT workflow_state, COUNT(*) c FROM {$pre}ai_acc_events GROUP BY workflow_state", ARRAY_A);
        $smap = [];
        foreach ($states as $s) $smap[$s['workflow_state']] = (int) $s['c'];
        $failed = (int) ($smap['failed'] ?? 0);
        $proc   = (int) ($smap['processing'] ?? 0);
        $pnc    = (int) ($smap['posted'] ?? 0); // posted ولی هنوز confirm نشده
        $fail_rows = [];
        if ($failed) {
            $rows = $wpdb->get_results("SELECT id, event_type, error_message FROM {$pre}ai_acc_events WHERE workflow_state='failed' ORDER BY id DESC LIMIT {$LIM}", ARRAY_A);
            foreach ($rows as $r) $fail_rows[] = ['#' . $r['id'], $r['event_type'], mb_substr((string) $r['error_message'], 0, 120)];
        }
        $controls[] = [
            'key' => 'health', 'title' => '🩺 سلامتِ اسناد',
            'severity' => $failed ? 'fail' : (($proc + $pnc) ? 'warn' : 'ok'),
            'count' => $failed,
            'summary' => "خطا: {$failed} · در پردازش: {$proc} · پست‌شدهٔ تأییدنشده: {$pnc}",
            'headers' => ['سند', 'نوع رویداد', 'پیام خطا'], 'rows' => $fail_rows,
        ];

        // ── D) یکپارچگیِ داده ──
        $int_rows = []; $int_count = 0;
        $orphans = $wpdb->get_results(
            "SELECT f.id, f.from_account_id, f.to_account_id
             FROM {$pre}ai_acc_flows f
             LEFT JOIN {$pre}ai_acc_accounts a1 ON a1.account_id = f.from_account_id
             LEFT JOIN {$pre}ai_acc_accounts a2 ON a2.account_id = f.to_account_id
             WHERE a1.account_id IS NULL OR a2.account_id IS NULL LIMIT {$LIM}", ARRAY_A
        );
        foreach ($orphans as $o) { $int_count++; $int_rows[] = ['آرتیکلِ یتیم', 'flow #' . $o['id'] . ' → حسابِ نامعتبر']; }
        $empty = $wpdb->get_results(
            "SELECT e.id, e.event_type FROM {$pre}ai_acc_events e
             WHERE e.workflow_state IN ('posted','confirmed')
               AND NOT EXISTS (SELECT 1 FROM {$pre}ai_acc_flows f WHERE f.event_id = e.id) LIMIT {$LIM}", ARRAY_A
        );
        foreach ($empty as $e) { $int_count++; $int_rows[] = ['سندِ بدونِ آرتیکل', '#' . $e['id'] . ' ' . $e['event_type']]; }
        $controls[] = [
            'key' => 'integrity', 'title' => '🧩 یکپارچگیِ داده',
            'severity' => $int_count ? 'fail' : 'ok', 'count' => $int_count,
            'summary' => $int_count ? "{$int_count} مشکلِ یکپارچگی" : 'یکپارچگی سالم است',
            'headers' => ['نوع مشکل', 'جزئیات'], 'rows' => $int_rows,
        ];

        // ── E) ناهنجاریِ مبلغ (بیشینه > ۵ برابرِ میانگین) ──
        $sig_stats = $wpdb->get_results(
            "SELECT from_account_id, to_account_id, COUNT(*) c, AVG(amount) avg_amt, MAX(amount) max_amt
             FROM {$pre}ai_acc_flows WHERE is_confirmed = 1
             GROUP BY from_account_id, to_account_id HAVING c >= 3", ARRAY_A
        );
        $out_rows = [];
        foreach ($sig_stats as $s) {
            if ((float) $s['avg_amt'] > 0 && (float) $s['max_amt'] > (float) $s['avg_amt'] * 5) {
                if (count($out_rows) < $LIM) {
                    $out_rows[] = [$nm($s['from_account_id']) . ' → ' . $nm($s['to_account_id']), $fmt($s['avg_amt']), $fmt($s['max_amt']), (int) $s['c']];
                }
            }
        }
        $controls[] = [
            'key' => 'outliers', 'title' => '📈 ناهنجاریِ مبلغ',
            'severity' => count($out_rows) ? 'warn' : 'ok', 'count' => count($out_rows),
            'summary' => count($out_rows) ? count($out_rows) . ' جفت‌حساب با مبلغِ پرت' : 'مبلغِ پرتی یافت نشد',
            'headers' => ['جفت حساب', 'میانگین', 'بیشینه', 'تعداد'], 'rows' => $out_rows,
        ];

        // خلاصه
        $total_flows = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$pre}ai_acc_flows WHERE is_confirmed = 1");
        $fails = 0; $warns = 0;
        foreach ($controls as $c) { if ($c['severity'] === 'fail') $fails++; elseif ($c['severity'] === 'warn') $warns++; }

        return ['success' => true, 'data' => [
            'controls' => $controls,
            'summary'  => [
                'confirmed_flows' => $total_flows,
                'controls'        => count($controls),
                'fails'           => $fails,
                'warns'           => $warns,
                'overall'         => $fails ? 'fail' : ($warns ? 'warn' : 'ok'),
            ],
        ]];
    }

    public static function ledger(WP_REST_Request $req): array {
        global $wpdb;
        $account_id = (int) ($req->get_param('account_id') ?? 0);
        if (!$account_id) return ['success' => false, 'message' => 'account_id الزامی است'];

        // direction از دیدِ این حساب: ورودی (to) یا خروجی (from)
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT f.id AS flow_id, e.id AS event_id, e.event_type,
                    f.amount,
                    CASE WHEN f.to_account_id = %d THEN 'in' ELSE 'out' END AS direction,
                    f.description, f.created_at
             FROM {$wpdb->prefix}ai_acc_flows f
             JOIN {$wpdb->prefix}ai_acc_events e ON e.id = f.event_id
             WHERE (f.to_account_id = %d OR f.from_account_id = %d) AND f.is_confirmed = 1
             ORDER BY f.id ASC",
            $account_id, $account_id, $account_id
        ), ARRAY_A);

        // مانده تجمعی (ورودی + ، خروجی −)
        $balance = 0;
        foreach ($rows as &$row) {
            $balance += ($row['direction'] === 'in' ? (float)$row['amount'] : -(float)$row['amount']);
            $row['running_balance'] = $balance;
        }

        return ['success' => true, 'data' => $rows ?: []];
    }

    public static function flow_journal(WP_REST_Request $req): array {
        global $wpdb;
        $event_id   = (int) ($req->get_param('event_id') ?? 0);
        $page       = max(1, (int) ($req->get_param('page') ?? 1));
        $size       = max(1, min(200, (int) ($req->get_param('size') ?? 50)));
        $offset     = ($page - 1) * $size;

        $conds = [];
        if ($event_id)                          $conds[] = $wpdb->prepare('f.event_id = %d', $event_id);
        if ($req->get_param('is_confirmed') !== null && $req->get_param('is_confirmed') !== '')
                                                $conds[] = $wpdb->prepare('f.is_confirmed = %d', (int) $req->get_param('is_confirmed'));
        if ($req->get_param('date_from'))       $conds[] = $wpdb->prepare('f.created_at >= %s', $req->get_param('date_from') . ' 00:00:00');
        if ($req->get_param('date_to'))         $conds[] = $wpdb->prepare('f.created_at <= %s', $req->get_param('date_to')   . ' 23:59:59');
        $where = $conds ? 'WHERE ' . implode(' AND ', $conds) : '';

        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT f.id AS flow_id, e.id AS event_id, e.event_type, e.description, e.workflow_state,
                    fa.account_code AS from_code, fa.account_name AS from_account_name,
                    ta.account_code AS to_code,   ta.account_name AS to_account_name,
                    f.amount, f.is_confirmed, f.created_at
             FROM {$wpdb->prefix}ai_acc_flows f
             JOIN {$wpdb->prefix}ai_acc_events e ON e.id = f.event_id
             LEFT JOIN {$wpdb->prefix}ai_acc_accounts fa ON fa.account_id = f.from_account_id
             LEFT JOIN {$wpdb->prefix}ai_acc_accounts ta ON ta.account_id = f.to_account_id
             {$where}
             ORDER BY e.id DESC, f.id ASC
             LIMIT %d OFFSET %d",
            $size, $offset
        ), ARRAY_A);

        return ['success' => true, 'data' => $rows ?: []];
    }

    // =========================================================
    // DIMENSIONS
    // =========================================================

    /**
     * تعریفِ ابعاد (ai_acc_dimensions). join با گروه روی group_id (عددی — بدون مشکلِ collation).
     * فیلترِ اختیاری: group_id
     */
    public static function get_dimensions(WP_REST_Request $req): array {
        global $wpdb; $pre = $wpdb->prefix;
        $gid   = $req->get_param('group_id');
        $where = ($gid !== null && $gid !== '') ? $wpdb->prepare('WHERE d.group_id = %d', (int) $gid) : '';
        $rows  = $wpdb->get_results(
            "SELECT d.id, d.dim_code, d.dim_type, d.dim_name,
                    COALESCE(d.name, d.dim_name) AS name,
                    d.group_id, d.data_type, d.is_active,
                    g.group_code, g.group_name
             FROM {$pre}ai_acc_dimensions d
             LEFT JOIN {$pre}ai_acc_dimension_groups g ON g.id = d.group_id
             {$where}
             ORDER BY d.dim_code ASC",
            ARRAY_A
        );
        return ['success' => true, 'data' => $rows ?: []];
    }

    public static function create_dimension(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $t = $pre . 'ai_acc_dimensions';

        $name = sanitize_text_field($req->get_param('dim_name') ?? $req->get_param('name') ?? '');
        $gid  = $req->get_param('group_id') ? (int) $req->get_param('group_id') : null;
        if ($name === '') {
            return new WP_REST_Response(['success' => false, 'message' => 'نام بُعد الزامی است'], 400);
        }
        // dim_type را از کدِ گروه پر کن (اگر group_id داده شده)
        $dim_type = sanitize_text_field($req->get_param('dim_type') ?? '');
        if ($dim_type === '' && $gid) {
            $dim_type = (string) $wpdb->get_var($wpdb->prepare("SELECT group_code FROM {$pre}ai_acc_dimension_groups WHERE id = %d", $gid));
        }
        $code = sanitize_text_field($req->get_param('dim_code') ?? '');
        if ($code === '') {
            $seq  = (int) $wpdb->get_var("SELECT COALESCE(MAX(id),0) FROM {$t}") + 1;
            $code = 'DIM-' . str_pad((string) $seq, 4, '0', STR_PAD_LEFT);
        }
        $dt = strtoupper(sanitize_text_field($req->get_param('data_type') ?: 'STRING'));
        if (!in_array($dt, ['STRING', 'INT', 'BIGINT', 'DECIMAL'], true)) $dt = 'STRING';

        $wpdb->insert($t, [
            'dim_code'  => $code,
            'dim_type'  => $dim_type ?: null,
            'dim_name'  => $name,
            'name'      => $name,
            'entity_id' => 0,
            'group_id'  => $gid,
            'data_type' => $dt,
            'is_active' => 1,
        ]);
        if ($wpdb->last_error) {
            return new WP_REST_Response(['success' => false, 'message' => $wpdb->last_error], 500);
        }
        return new WP_REST_Response(['success' => true, 'id' => $wpdb->insert_id, 'dim_code' => $code], 201);
    }

    public static function update_dimension(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $id = (int) $req->get_param('id');
        $data = [];
        $name = $req->get_param('dim_name') ?? $req->get_param('name');
        if ($name !== null)                  { $data['dim_name'] = sanitize_text_field($name); $data['name'] = $data['dim_name']; }
        if ($req->has_param('dim_code'))     $data['dim_code']  = sanitize_text_field($req->get_param('dim_code'));
        if ($req->has_param('group_id'))     {
            $gid = $req->get_param('group_id') ? (int) $req->get_param('group_id') : null;
            $data['group_id'] = $gid;
            $data['dim_type'] = $gid ? (string) $wpdb->get_var($wpdb->prepare("SELECT group_code FROM {$pre}ai_acc_dimension_groups WHERE id = %d", $gid)) : null;
        }
        if ($req->has_param('data_type')) {
            $dt = strtoupper(sanitize_text_field($req->get_param('data_type')));
            if (in_array($dt, ['STRING', 'INT', 'BIGINT', 'DECIMAL'], true)) $data['data_type'] = $dt;
        }
        if ($req->has_param('is_active'))    $data['is_active'] = (int) $req->get_param('is_active');
        if (!empty($data)) $wpdb->update($pre . 'ai_acc_dimensions', $data, ['id' => $id]);
        return new WP_REST_Response(['success' => true], 200);
    }

    public static function delete_dimension(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $id = (int) $req->get_param('id');
        // مقادیرِ این بُعد هم حذف شوند
        $wpdb->delete($pre . 'ai_acc_dimension_values', ['dim_id' => $id]);
        $wpdb->delete($pre . 'ai_acc_dimensions', ['id' => $id]);
        return new WP_REST_Response(['success' => true], 200);
    }

    // ── مقادیرِ بُعد (ai_acc_dimension_values) ──
    public static function get_dim_values(WP_REST_Request $req): array {
        global $wpdb; $pre = $wpdb->prefix;
        $dim_id = (int) ($req->get_param('dim_id') ?? 0);
        if (!$dim_id) return ['success' => false, 'message' => 'dim_id الزامی است'];
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, dim_id, value_code, value_name, parent_value_id, is_active
             FROM {$pre}ai_acc_dimension_values WHERE dim_id = %d ORDER BY value_code ASC", $dim_id
        ), ARRAY_A);
        return ['success' => true, 'data' => $rows ?: []];
    }

    public static function create_dim_value(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $t = $pre . 'ai_acc_dimension_values';
        $dim_id = (int) $req->get_param('dim_id');
        $name   = sanitize_text_field($req->get_param('value_name') ?? '');
        if (!$dim_id || $name === '') {
            return new WP_REST_Response(['success' => false, 'message' => 'بُعد و نامِ مقدار الزامی‌اند'], 400);
        }
        $code = sanitize_text_field($req->get_param('value_code') ?? '');
        if ($code === '') {
            $seq  = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$t} WHERE dim_id = %d", $dim_id)) + 1;
            $code = 'V' . str_pad((string) $seq, 3, '0', STR_PAD_LEFT);
        }
        $parent = $req->get_param('parent_value_id') ? (int) $req->get_param('parent_value_id') : null;
        $wpdb->insert($t, [
            'dim_id'          => $dim_id,
            'value_code'      => $code,
            'value_name'      => $name,
            'parent_value_id' => $parent,
            'is_active'       => 1,
        ]);
        if ($wpdb->last_error) {
            return new WP_REST_Response(['success' => false, 'message' => $wpdb->last_error], 500);
        }
        return new WP_REST_Response(['success' => true, 'id' => $wpdb->insert_id, 'value_code' => $code], 201);
    }

    public static function update_dim_value(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $id = (int) $req->get_param('id');
        $data = [];
        if ($req->has_param('value_name')) $data['value_name'] = sanitize_text_field($req->get_param('value_name'));
        if ($req->has_param('value_code')) $data['value_code'] = sanitize_text_field($req->get_param('value_code'));
        if ($req->has_param('is_active'))  $data['is_active']  = (int) $req->get_param('is_active');
        if (!empty($data)) $wpdb->update($pre . 'ai_acc_dimension_values', $data, ['id' => $id]);
        return new WP_REST_Response(['success' => true], 200);
    }

    public static function delete_dim_value(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $wpdb->delete($wpdb->prefix . 'ai_acc_dimension_values', ['id' => (int) $req->get_param('id')]);
        return new WP_REST_Response(['success' => true], 200);
    }

    /** کلِ درختِ ابعاد در یک تماس: گروه‌ها ← ابعاد ← مقادیر */
    public static function dimension_tree(WP_REST_Request $req): array {
        global $wpdb; $pre = $wpdb->prefix;
        $groups = $wpdb->get_results(
            "SELECT id, group_code, group_name, sort_order FROM {$pre}ai_acc_dimension_groups WHERE is_active = 1 ORDER BY sort_order ASC", ARRAY_A
        ) ?: [];
        $dims = $wpdb->get_results(
            "SELECT id, dim_code, dim_name, COALESCE(name, dim_name) AS name, group_id, data_type, is_active
             FROM {$pre}ai_acc_dimensions ORDER BY dim_code ASC", ARRAY_A
        ) ?: [];
        $vals = $wpdb->get_results(
            "SELECT id, dim_id, value_code, value_name, is_active FROM {$pre}ai_acc_dimension_values ORDER BY value_code ASC", ARRAY_A
        ) ?: [];

        $vals_by_dim = [];
        foreach ($vals as $v) $vals_by_dim[(int) $v['dim_id']][] = $v;
        $dims_by_group = [];
        foreach ($dims as $d) { $d['values'] = $vals_by_dim[(int) $d['id']] ?? []; $dims_by_group[(int) $d['group_id']][] = $d; }
        foreach ($groups as &$g) $g['dimensions'] = $dims_by_group[(int) $g['id']] ?? [];
        unset($g);

        return ['success' => true, 'data' => $groups];
    }


    // =========================================================
    // POST EVENT TO LEDGER  (Acc_Event_Engine)
    // =========================================================

    /**
     * هستهٔ پست رویداد — Acc_Event_Engine
     */
    public static function post_event_core( int $id ): array {
        return Acc_Event_Engine::post_event( $id );
    }

    /**
     * پست رویداد به دفتر (REST)
     */
    public static function post_event_to_ledger( WP_REST_Request $req ): WP_REST_Response {
        $id = (int) $req->get_param( 'id' );
        $result = Acc_Event_Engine::post_event( $id );
        $http = (int) ( $result['http'] ?? ( $result['success'] ? 200 : 400 ) );
        unset( $result['http'] );
        return new WP_REST_Response( $result, $http );
    }

    // =========================================================
    // EVENT TEMPLATES
    // =========================================================

    // =========================================================
    // ACCOUNTING GRAPH
    // =========================================================

    /**
     * GET /accounting/graph
     * گراف حسابداری: نودها (حساب‌ها) + یال‌های Template + یال‌های جریان واقعی
     */
    public static function get_graph( WP_REST_Request $req ): array {
        global $wpdb;
        $pre = $wpdb->prefix;

        // ── نودها: همه حساب‌های برگ (leaf) یا دارای جریان ─────────
        $accounts = $wpdb->get_results(
            "SELECT account_id AS id, account_code AS code, account_name AS name, account_type AS type
             FROM {$pre}ai_acc_accounts
             ORDER BY account_code ASC",
            ARRAY_A
        );

        // ── یال‌های Template (dr → cr از mapping_logic) ────────────
        $templates = $wpdb->get_results(
            "SELECT id, template_name, event_type, mapping_logic FROM {$pre}ai_acc_event_templates WHERE is_active = 1",
            ARRAY_A
        );

        $template_edges = [];
        $tpl_edge_key   = [];
        foreach ( $templates as $tpl ) {
            $logic = json_decode( $tpl['mapping_logic'] ?? '{}', true );
            $pairs = $logic['pairs'] ?? [];
            foreach ( $pairs as $pair ) {
                $dr = (int) ( $pair['dr_gl_id'] ?? 0 );
                $cr = (int) ( $pair['cr_gl_id'] ?? 0 );
                if ( ! $dr || ! $cr ) continue;
                $key = "{$dr}_{$cr}";
                if ( ! isset( $tpl_edge_key[ $key ] ) ) {
                    $tpl_edge_key[ $key ] = count( $template_edges );
                    $template_edges[]     = [
                        'from'    => $dr,
                        'to'      => $cr,
                        'count'   => 0,
                        'labels'  => [],
                    ];
                }
                $idx = $tpl_edge_key[ $key ];
                $template_edges[ $idx ]['count']++;
                $template_edges[ $idx ]['labels'][] = $tpl['event_type'];
            }
        }

        // ── یال‌های جریان واقعی (تأییدشده) ────────────────────────
        $flow_rows = $wpdb->get_results(
            "SELECT from_account_id AS `from`, to_account_id AS `to`,
                    COUNT(*) AS flow_count, SUM(amount) AS total_amount
             FROM {$pre}ai_acc_flows
             WHERE is_confirmed = 1 AND from_account_id > 0 AND to_account_id > 0
             GROUP BY from_account_id, to_account_id",
            ARRAY_A
        );

        // ── آمار هر حساب ───────────────────────────────────────────
        $stats = $wpdb->get_results(
            "SELECT a.account_id AS account_id,
                    SUM(CASE WHEN f.to_account_id   = a.account_id AND f.is_confirmed=1 THEN f.amount ELSE 0 END) AS total_in,
                    SUM(CASE WHEN f.from_account_id = a.account_id AND f.is_confirmed=1 THEN f.amount ELSE 0 END) AS total_out
             FROM {$pre}ai_acc_accounts a
             LEFT JOIN {$pre}ai_acc_flows f
                    ON f.to_account_id = a.account_id OR f.from_account_id = a.account_id
             GROUP BY a.account_id",
            ARRAY_A
        );
        $stats_map = [];
        foreach ( $stats as $s ) {
            $stats_map[ (int) $s['account_id'] ] = [
                'total_in'  => (float) $s['total_in'],
                'total_out' => (float) $s['total_out'],
            ];
        }
        foreach ( $accounts as &$acc ) {
            $s = $stats_map[ (int) $acc['id'] ] ?? ['total_in' => 0, 'total_out' => 0];
            $acc['total_in']  = $s['total_in'];
            $acc['total_out'] = $s['total_out'];
        }
        unset( $acc );

        return [
            'success'        => true,
            'nodes'          => $accounts,
            'template_edges' => array_values( $template_edges ),
            'flow_edges'     => $flow_rows ?: [],
        ];
    }

    public static function get_templates(WP_REST_Request $req): array {
        global $wpdb;
        $t           = $wpdb->prefix . 'ai_acc_event_templates';
        $event_type  = sanitize_text_field($req->get_param('event_type') ?? '');
        $active_only = (bool) ($req->get_param('active') ?? false);

        $where_parts = [];
        $args        = [];

        if ($event_type !== '') {
            $where_parts[] = 'event_type = %s';
            $args[]        = $event_type;
        }
        if ($active_only) {
            $where_parts[] = 'is_active = 1';
        }

        $where = $where_parts ? 'WHERE ' . implode(' AND ', $where_parts) : '';

        if ($args) {
            $sql = $wpdb->prepare("SELECT * FROM {$t} {$where} ORDER BY event_type ASC, version DESC", ...$args);
        } else {
            $sql = "SELECT * FROM {$t} {$where} ORDER BY event_type ASC, version DESC";
        }

        $rows = $wpdb->get_results($sql, ARRAY_A);

        // Decode mapping_logic JSON for each row
        foreach ($rows as &$row) {
            if (!empty($row['mapping_logic'])) {
                $decoded = json_decode($row['mapping_logic'], true);
                if ($decoded !== null) $row['mapping_logic'] = $decoded;
            }
        }

        return ['success' => true, 'data' => $rows ?: []];
    }

    public static function get_template(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $id  = (int) $req->get_param('id');
        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$wpdb->prefix}ai_acc_event_templates WHERE id = %d", $id),
            ARRAY_A
        );
        if (!$row) {
            return new WP_REST_Response(['success' => false, 'message' => 'یافت نشد'], 404);
        }
        if (!empty($row['mapping_logic'])) {
            $decoded = json_decode($row['mapping_logic'], true);
            if ($decoded !== null) $row['mapping_logic'] = $decoded;
        }
        return new WP_REST_Response(['success' => true, 'data' => $row], 200);
    }

    public static function create_template(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $t          = $wpdb->prefix . 'ai_acc_event_templates';
        $event_type = sanitize_text_field($req->get_param('event_type'));
        $logic      = $req->get_param('mapping_logic');

        if (!$event_type) {
            return new WP_REST_Response(['success' => false, 'message' => 'event_type الزامی است'], 400);
        }

        // Auto-increment version
        $max_ver = (int) $wpdb->get_var(
            $wpdb->prepare("SELECT COALESCE(MAX(version),0) FROM {$t} WHERE event_type = %s", $event_type)
        );

        $logic_json = is_string($logic) ? $logic : wp_json_encode($logic ?? ['pairs' => []]);

        // Validate JSON structure
        $decoded = json_decode($logic_json, true);
        if ($decoded === null || !isset($decoded['pairs'])) {
            return new WP_REST_Response([
                'success' => false,
                'message' => 'mapping_logic باید JSON معتبر با کلید "pairs" باشد',
            ], 400);
        }

        $wpdb->insert($t, [
            'event_type'    => $event_type,
            'version'       => $max_ver + 1,
            'template_name' => sanitize_text_field($req->get_param('template_name') ?? ''),
            'mapping_logic' => $logic_json,
            'description'   => sanitize_textarea_field($req->get_param('description') ?? ''),
            'is_active'     => (int) ($req->get_param('is_active') ?? 1),
        ]);

        if ($wpdb->last_error) {
            return new WP_REST_Response(['success' => false, 'message' => $wpdb->last_error], 500);
        }

        return new WP_REST_Response(['success' => true, 'id' => $wpdb->insert_id, 'version' => $max_ver + 1], 201);
    }

    public static function update_template(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $t    = $wpdb->prefix . 'ai_acc_event_templates';
        $id   = (int) $req->get_param('id');
        $data = [];

        if ($req->has_param('template_name')) $data['template_name'] = sanitize_text_field($req->get_param('template_name'));
        if ($req->has_param('description'))   $data['description']   = sanitize_textarea_field($req->get_param('description'));
        if ($req->has_param('is_active'))     $data['is_active']     = (int) $req->get_param('is_active');

        if ($req->has_param('mapping_logic')) {
            $logic      = $req->get_param('mapping_logic');
            $logic_json = is_string($logic) ? $logic : wp_json_encode($logic);
            $decoded    = json_decode($logic_json, true);
            if ($decoded === null || !isset($decoded['pairs'])) {
                return new WP_REST_Response([
                    'success' => false,
                    'message' => 'mapping_logic JSON نامعتبر است',
                ], 400);
            }
            $data['mapping_logic'] = $logic_json;
        }

        if (!empty($data)) {
            $wpdb->update($t, $data, ['id' => $id]);
        }

        return new WP_REST_Response(['success' => true], 200);
    }

    public static function delete_template(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $id = (int) $req->get_param('id');
        // Soft delete — just deactivate
        $wpdb->update(
            $wpdb->prefix . 'ai_acc_event_templates',
            ['is_active' => 0],
            ['id' => $id]
        );
        return new WP_REST_Response(['success' => true], 200);
    }

    // =========================================================
    // ARTICLE BANK  (بانک آرتیکل)
    // =========================================================

    /** امضای آرتیکل برای تطبیق با pairهای template */
    private static function _article_sig($dr, $cr, $src): string {
        return (int) $dr . '|' . (int) $cr . '|' . (string) $src;
    }

    /** شمارشِ تعدادِ template‌های فعالی که هر امضا را دارند */
    private static function _template_sig_usage(): array {
        global $wpdb; $pre = $wpdb->prefix;
        $tpls = $wpdb->get_results(
            "SELECT id, mapping_logic FROM {$pre}ai_acc_event_templates WHERE is_active = 1", ARRAY_A
        );
        $usage = [];
        foreach ($tpls as $t) {
            $logic = is_string($t['mapping_logic']) ? json_decode($t['mapping_logic'], true) : $t['mapping_logic'];
            $pairs = (is_array($logic) && !empty($logic['pairs'])) ? $logic['pairs'] : [];
            $seen = [];
            foreach ($pairs as $p) {
                $sig = self::_article_sig($p['dr_gl_id'] ?? 0, $p['cr_gl_id'] ?? 0, $p['source_field'] ?? '');
                if (isset($seen[$sig])) continue;
                $seen[$sig] = 1;
                $usage[$sig] = ($usage[$sig] ?? 0) + 1;
            }
        }
        return $usage;
    }

    public static function get_articles(WP_REST_Request $req): array {
        global $wpdb; $pre = $wpdb->prefix;
        $t = $pre . 'ai_acc_articles';

        $conds = []; $args = [];
        if ($v = $req->get_param('category')) { $conds[] = 'a.category = %s'; $args[] = sanitize_text_field($v); }
        if ($v = $req->get_param('search'))   { $conds[] = '(a.label LIKE %s OR a.code LIKE %s)'; $s = '%' . $wpdb->esc_like($v) . '%'; $args[] = $s; $args[] = $s; }
        $active = $req->get_param('active');
        if ($active !== null && $active !== '') { $conds[] = 'a.is_active = %d'; $args[] = (int) $active; }
        $where = $conds ? 'WHERE ' . implode(' AND ', $conds) : '';

        $sql = "SELECT a.*,
                       dr.account_code AS dr_code, dr.account_name AS dr_name,
                       cr.account_code AS cr_code, cr.account_name AS cr_name
                FROM {$t} a
                LEFT JOIN {$pre}ai_acc_accounts dr ON dr.account_id = a.dr_account_id
                LEFT JOIN {$pre}ai_acc_accounts cr ON cr.account_id = a.cr_account_id
                {$where}
                ORDER BY a.category ASC, a.code ASC";
        $rows = $args ? $wpdb->get_results($wpdb->prepare($sql, ...$args), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);

        $usage = self::_template_sig_usage();
        foreach ($rows as &$r) {
            $sig = self::_article_sig($r['dr_account_id'], $r['cr_account_id'], $r['amount_source']);
            $r['usage_count'] = $usage[$sig] ?? 0;
        }
        return ['success' => true, 'data' => $rows ?: []];
    }

    public static function create_article(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $t = $pre . 'ai_acc_articles';

        $label = sanitize_text_field($req->get_param('label') ?? '');
        $dr    = (int) $req->get_param('dr_account_id');
        $cr    = (int) $req->get_param('cr_account_id');
        if ($label === '' || !$dr || !$cr) {
            return new WP_REST_Response(['success' => false, 'message' => 'برچسب، حساب بدهکار و بستانکار الزامی‌اند'], 400);
        }
        // اعتبارِ حساب‌ها روی account_id
        foreach ([$dr, $cr] as $acc_id) {
            if (!$wpdb->get_var($wpdb->prepare("SELECT 1 FROM {$pre}ai_acc_accounts WHERE account_id = %d", $acc_id))) {
                return new WP_REST_Response(['success' => false, 'message' => "حساب #{$acc_id} یافت نشد"], 400);
            }
        }
        $code = sanitize_text_field($req->get_param('code') ?? '');
        if ($code === '') {
            $seq  = (int) $wpdb->get_var("SELECT COALESCE(MAX(id),0) FROM {$t}") + 1;
            $code = 'ART-' . str_pad((string) $seq, 4, '0', STR_PAD_LEFT);
        }
        $wpdb->insert($t, [
            'code'          => $code,
            'label'         => $label,
            'dr_account_id' => $dr,
            'cr_account_id' => $cr,
            'amount_source' => sanitize_text_field($req->get_param('amount_source') ?: 'amount'),
            'category'      => sanitize_text_field($req->get_param('category') ?? '') ?: null,
            'description'   => sanitize_textarea_field($req->get_param('description') ?? ''),
            'is_active'     => 1,
            'created_by'    => get_current_user_id() ?: null,
        ]);
        if ($wpdb->last_error) {
            return new WP_REST_Response(['success' => false, 'message' => $wpdb->last_error], 500);
        }
        return new WP_REST_Response(['success' => true, 'id' => $wpdb->insert_id, 'code' => $code], 201);
    }

    public static function update_article(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $id = (int) $req->get_param('id');
        $data = [];
        if ($req->has_param('label'))         $data['label']         = sanitize_text_field($req->get_param('label'));
        if ($req->has_param('dr_account_id')) $data['dr_account_id'] = (int) $req->get_param('dr_account_id');
        if ($req->has_param('cr_account_id')) $data['cr_account_id'] = (int) $req->get_param('cr_account_id');
        if ($req->has_param('amount_source')) $data['amount_source'] = sanitize_text_field($req->get_param('amount_source'));
        if ($req->has_param('category'))      $data['category']      = sanitize_text_field($req->get_param('category')) ?: null;
        if ($req->has_param('description'))   $data['description']   = sanitize_textarea_field($req->get_param('description'));
        if ($req->has_param('is_active'))     $data['is_active']     = (int) $req->get_param('is_active');
        if (!empty($data)) $wpdb->update($pre . 'ai_acc_articles', $data, ['id' => $id]);
        return new WP_REST_Response(['success' => true], 200);
    }

    public static function delete_article(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $wpdb->delete($pre . 'ai_acc_articles', ['id' => (int) $req->get_param('id')]);
        return new WP_REST_Response(['success' => true], 200);
    }

    /**
     * استخراجِ آرتیکل‌ها از template‌های فعال:
     * هر pairِ یکتا (dr|cr|source_field) که هنوز در بانک نیست، به آرتیکل تبدیل می‌شود.
     */
    public static function extract_articles(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $t = $pre . 'ai_acc_articles';

        $existing = $wpdb->get_results("SELECT dr_account_id, cr_account_id, amount_source FROM {$t}", ARRAY_A);
        $have = [];
        foreach ($existing as $e) {
            $have[self::_article_sig($e['dr_account_id'], $e['cr_account_id'], $e['amount_source'])] = true;
        }
        $seq = (int) $wpdb->get_var("SELECT COALESCE(MAX(id),0) FROM {$t}");

        $tpls = $wpdb->get_results(
            "SELECT event_type, template_name, mapping_logic FROM {$pre}ai_acc_event_templates WHERE is_active = 1",
            ARRAY_A
        );
        $created = 0; $skipped = 0;
        foreach ($tpls as $tp) {
            $logic = is_string($tp['mapping_logic']) ? json_decode($tp['mapping_logic'], true) : $tp['mapping_logic'];
            $pairs = (is_array($logic) && !empty($logic['pairs'])) ? $logic['pairs'] : [];
            foreach ($pairs as $p) {
                $dr = (int) ($p['dr_gl_id'] ?? 0);
                $cr = (int) ($p['cr_gl_id'] ?? 0);
                if (!$dr || !$cr) continue;
                $src = (string) ($p['source_field'] ?? '');
                $sig = self::_article_sig($dr, $cr, $src);
                if (isset($have[$sig])) { $skipped++; continue; }
                $have[$sig] = true;
                $seq++;
                $label = $p['desc'] ?? ($p['description'] ?? ($tp['template_name'] ?: $tp['event_type']));
                $wpdb->insert($t, [
                    'code'          => 'ART-' . str_pad((string) $seq, 4, '0', STR_PAD_LEFT),
                    'label'         => mb_substr((string) $label, 0, 200),
                    'dr_account_id' => $dr,
                    'cr_account_id' => $cr,
                    'amount_source' => $src,
                    'category'      => $tp['event_type'],
                    'description'   => 'استخراج‌شده از ' . $tp['event_type'],
                    'is_active'     => 1,
                ]);
                if (!$wpdb->last_error) $created++;
            }
        }
        return new WP_REST_Response(['success' => true, 'created' => $created, 'skipped' => $skipped], 200);
    }

    // =========================================================
    // SUGGESTION ENGINE  (موتورِ پیشنهاد)
    // =========================================================

    /** نگاشتِ account_id → {code,name} برای فهرستی از شناسه‌ها */
    private static function _acc_map(array $ids): array {
        global $wpdb; $pre = $wpdb->prefix;
        $ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
        if (!$ids) return [];
        $ph = implode(',', array_fill(0, count($ids), '%d'));
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT account_id AS id, account_code AS code, account_name AS name
             FROM {$pre}ai_acc_accounts WHERE account_id IN ({$ph})", $ids
        ), ARRAY_A);
        $m = [];
        foreach ($rows as $r) $m[(int) $r['id']] = $r;
        return $m;
    }

    /** فهرستِ نوع‌رویدادهای شناخته‌شده (از template‌ها + اسناد) */
    public static function get_event_types(WP_REST_Request $req): array {
        global $wpdb; $pre = $wpdb->prefix;
        $rows = $wpdb->get_col(
            "SELECT DISTINCT event_type FROM {$pre}ai_acc_event_templates WHERE is_active = 1 AND event_type <> ''
             UNION
             SELECT DISTINCT event_type FROM {$pre}ai_acc_events WHERE event_type <> ''
             ORDER BY event_type ASC"
        );
        return ['success' => true, 'data' => $rows ?: []];
    }

    /**
     * پیشنهاد برای یک نوع‌رویداد:
     *   - template فعال (با pairهای resolve‌شده به نام حساب + لینک به بانک آرتیکل)
     *   - فیلدهای مبلغِ لازم (source_field)
     *   - آمارِ تاریخی
     *   - آرتیکل‌های پیشنهادیِ بانک (هم‌دسته)
     */
    public static function suggest_articles(WP_REST_Request $req): array {
        global $wpdb; $pre = $wpdb->prefix;
        $et = sanitize_text_field($req->get_param('event_type') ?? '');

        $out = [
            'event_type'        => $et,
            'template'          => null,
            'source_fields'     => [],
            'history'           => ['posted_count' => 0, 'last_used' => null],
            'suggested_articles'=> [],
        ];
        if ($et === '') return ['success' => true, 'data' => $out];

        // template فعال
        $tpl = $wpdb->get_row($wpdb->prepare(
            "SELECT id, version, template_name, mapping_logic
             FROM {$pre}ai_acc_event_templates
             WHERE event_type = %s AND is_active = 1 ORDER BY version DESC LIMIT 1", $et
        ), ARRAY_A);

        if ($tpl) {
            $logic = is_string($tpl['mapping_logic']) ? json_decode($tpl['mapping_logic'], true) : $tpl['mapping_logic'];
            $pairs = (is_array($logic) && !empty($logic['pairs'])) ? $logic['pairs'] : [];

            $acc_ids = [];
            foreach ($pairs as $p) { $acc_ids[] = $p['dr_gl_id'] ?? 0; $acc_ids[] = $p['cr_gl_id'] ?? 0; }
            $acc_map = self::_acc_map($acc_ids);

            // امضای آرتیکل‌های بانک برای لینک‌دادن
            $arts = $wpdb->get_results("SELECT code, label, dr_account_id, cr_account_id, amount_source FROM {$pre}ai_acc_articles WHERE is_active = 1", ARRAY_A);
            $art_sig = [];
            foreach ($arts as $a) $art_sig[self::_article_sig($a['dr_account_id'], $a['cr_account_id'], $a['amount_source'])] = $a;

            $out_pairs = []; $fields = [];
            foreach ($pairs as $p) {
                $dr = (int) ($p['dr_gl_id'] ?? 0);
                $cr = (int) ($p['cr_gl_id'] ?? 0);
                $src = (string) ($p['source_field'] ?? '');
                if ($src !== '' && !in_array($src, $fields, true)) $fields[] = $src;
                $matched = $art_sig[self::_article_sig($dr, $cr, $src)] ?? null;
                $out_pairs[] = [
                    'dr_account_id' => $dr,
                    'dr_code'       => $acc_map[$dr]['code'] ?? '',
                    'dr_name'       => $acc_map[$dr]['name'] ?? ('#' . $dr),
                    'cr_account_id' => $cr,
                    'cr_code'       => $acc_map[$cr]['code'] ?? '',
                    'cr_name'       => $acc_map[$cr]['name'] ?? ('#' . $cr),
                    'source_field'  => $src,
                    'desc'          => $p['desc'] ?? ($p['description'] ?? ''),
                    'article_code'  => $matched['code'] ?? null,
                    'article_label' => $matched['label'] ?? null,
                ];
            }
            $out['template'] = [
                'id'            => (int) $tpl['id'],
                'version'       => (int) $tpl['version'],
                'template_name' => $tpl['template_name'],
                'pairs'         => $out_pairs,
            ];
            $out['source_fields'] = $fields;
        }

        // آمارِ تاریخی
        $hist = $wpdb->get_row($wpdb->prepare(
            "SELECT COUNT(*) AS posted_count, MAX(posted_at) AS last_used
             FROM {$pre}ai_acc_events
             WHERE event_type = %s AND workflow_state IN ('posted', 'confirmed')", $et
        ), ARRAY_A);
        $out['history'] = [
            'posted_count' => (int) ($hist['posted_count'] ?? 0),
            'last_used'    => $hist['last_used'] ?? null,
        ];

        // آرتیکل‌های پیشنهادیِ بانک (هم‌دسته با این نوع‌رویداد)
        $out['suggested_articles'] = $wpdb->get_results($wpdb->prepare(
            "SELECT a.id, a.code, a.label, a.amount_source,
                    dr.account_code AS dr_code, cr.account_code AS cr_code
             FROM {$pre}ai_acc_articles a
             LEFT JOIN {$pre}ai_acc_accounts dr ON dr.account_id = a.dr_account_id
             LEFT JOIN {$pre}ai_acc_accounts cr ON cr.account_id = a.cr_account_id
             WHERE a.is_active = 1 AND a.category = %s
             ORDER BY a.code ASC", $et
        ), ARRAY_A) ?: [];

        return ['success' => true, 'data' => $out];
    }

    // =========================================================
    // DIMENSION GROUPS
    // =========================================================

    public static function get_dim_groups(WP_REST_Request $req): array {
        global $wpdb;
        $rows = $wpdb->get_results(
            "SELECT * FROM {$wpdb->prefix}ai_acc_dimension_groups WHERE is_active = 1 ORDER BY sort_order ASC",
            ARRAY_A
        );
        return ['success' => true, 'data' => $rows ?: []];
    }

    public static function create_dim_group(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $wpdb->insert($wpdb->prefix . 'ai_acc_dimension_groups', [
            'group_code'  => sanitize_key($req->get_param('group_code')),
            'group_name'  => sanitize_text_field($req->get_param('group_name')),
            'description' => sanitize_textarea_field($req->get_param('description') ?? ''),
            'sort_order'  => (int) ($req->get_param('sort_order') ?? 0),
            'is_active'   => 1,
        ]);
        if ($wpdb->last_error) {
            return new WP_REST_Response(['success' => false, 'message' => $wpdb->last_error], 500);
        }
        return new WP_REST_Response(['success' => true, 'id' => $wpdb->insert_id], 201);
    }

    public static function update_dim_group(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $id = (int) $req->get_param('id');
        $data = [];
        if ($req->has_param('group_name'))  $data['group_name']  = sanitize_text_field($req->get_param('group_name'));
        if ($req->has_param('description')) $data['description'] = sanitize_textarea_field($req->get_param('description'));
        if ($req->has_param('sort_order'))  $data['sort_order']  = (int) $req->get_param('sort_order');
        if ($req->has_param('is_active'))   $data['is_active']   = (int) $req->get_param('is_active');
        if (!empty($data)) $wpdb->update($wpdb->prefix . 'ai_acc_dimension_groups', $data, ['id' => $id]);
        return new WP_REST_Response(['success' => true], 200);
    }

    public static function delete_dim_group(WP_REST_Request $req): WP_REST_Response {
        global $wpdb; $pre = $wpdb->prefix;
        $id = (int) $req->get_param('id');
        $cnt = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$pre}ai_acc_dimensions WHERE group_id = %d", $id));
        if ($cnt > 0) {
            return new WP_REST_Response(['success' => false, 'message' => 'این گروه دارای بُعد است؛ ابتدا ابعادش را حذف کنید'], 400);
        }
        $wpdb->delete($pre . 'ai_acc_dimension_groups', ['id' => $id]);
        return new WP_REST_Response(['success' => true], 200);
    }

    // =========================================================
    // GL–DIMENSION RULES
    // =========================================================

    public static function get_gl_dim_rules(WP_REST_Request $req): array {
        global $wpdb;
        $gl_id = (int) ($req->get_param('gl_id') ?? 0);
        $where = $gl_id ? $wpdb->prepare('WHERE r.gl_id = %d', $gl_id) : '';

        $rows = $wpdb->get_results(
            "SELECT r.id, r.gl_id, r.group_id, r.is_required,
                    a.account_code, a.account_name,
                    g.group_code, g.group_name
             FROM {$wpdb->prefix}ai_acc_gl_dim_rules r
             JOIN {$wpdb->prefix}ai_acc_accounts a           ON a.account_id = r.gl_id
             JOIN {$wpdb->prefix}ai_acc_dimension_groups g   ON g.id = r.group_id
             {$where}
             ORDER BY a.account_code ASC, g.sort_order ASC",
            ARRAY_A
        );
        return ['success' => true, 'data' => $rows ?: []];
    }

    public static function create_gl_dim_rule(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $wpdb->replace($wpdb->prefix . 'ai_acc_gl_dim_rules', [
            'gl_id'       => (int) $req->get_param('gl_id'),
            'group_id'    => (int) $req->get_param('group_id'),
            'is_required' => (int) ($req->get_param('is_required') ?? 1),
        ]);
        return new WP_REST_Response(['success' => true, 'id' => $wpdb->insert_id], 201);
    }

    public static function delete_gl_dim_rule(WP_REST_Request $req): WP_REST_Response {
        global $wpdb;
        $wpdb->delete($wpdb->prefix . 'ai_acc_gl_dim_rules', ['id' => (int) $req->get_param('id')]);
        return new WP_REST_Response(['success' => true], 200);
    }

    // =========================================================
    // SCHEMA MIGRATION v2
    // =========================================================

    public static function run_install_v2(WP_REST_Request $req): WP_REST_Response {
        $sql_file = ACC_PATH . 'includes/sql/accounting_engine_v2.sql';

        if (!file_exists($sql_file)) {
            return new WP_REST_Response([
                'success' => false,
                'message' => 'فایل SQL یافت نشد: ' . $sql_file,
            ], 404);
        }

        global $wpdb;
        $sql     = file_get_contents($sql_file);
        $errors  = [];
        $success = [];

        // Split on DELIMITER markers to handle stored procedures
        $sql = preg_replace('/^DELIMITER\s+\/\/\s*$/m', '', $sql);
        $sql = preg_replace('/^DELIMITER\s+;\s*$/m', '', $sql);
        $sql = str_replace('//', ';', $sql);

        // Split on semicolons (naive but sufficient for this controlled file)
        $statements = array_filter(
            array_map('trim', preg_split('/;\s*\n/', $sql)),
            fn($s) => strlen($s) > 10
        );

        foreach ($statements as $stmt) {
            $wpdb->query($stmt);
            if ($wpdb->last_error) {
                $errors[] = substr($wpdb->last_error, 0, 200);
            } else {
                $success[] = substr($stmt, 0, 60) . '...';
            }
        }

        return new WP_REST_Response([
            'success'        => empty($errors),
            'statements_run' => count($success),
            'errors'         => $errors,
        ], empty($errors) ? 200 : 207);
    }
}

add_action('rest_api_init', ['ACC_Accounting_Controller', 'register_routes']);
