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

require_once ACC_PATH . 'includes/helpers/pc-service.php';

/**
 * PC_Controller — Project Control REST API
 * Prefix: wp_pc_
 * Namespace: acc/v1/pc
 */
class PC_Controller {

    /** @var array<string, ?WP_REST_Response> */
    private static array $_access_cache = [];

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

        // ── Projects ──────────────────────────────────────────────
        register_rest_route( $ns, '/pc/projects', [
            [ 'methods' => 'GET',  'callback' => [ self::class, 'get_projects' ],   'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'POST', 'callback' => [ self::class, 'create_project' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/projects/(?P<id>\d+)', [
            [ 'methods' => 'GET',    'callback' => [ self::class, 'get_project' ],    'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'PUT',    'callback' => [ self::class, 'update_project' ], 'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'DELETE', 'callback' => [ self::class, 'delete_project' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/projects/(?P<id>\d+)/dashboard', [
            'methods' => 'GET', 'callback' => [ self::class, 'project_dashboard' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );
        register_rest_route( $ns, '/pc/projects/(?P<id>\d+)/workspace', [
            'methods' => 'GET', 'callback' => [ self::class, 'get_workspace' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );

        // ── Lands ──────────────────────────────────────────────────
        register_rest_route( $ns, '/pc/lands', [
            [ 'methods' => 'GET',  'callback' => [ self::class, 'get_lands' ],   'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'POST', 'callback' => [ self::class, 'create_land' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/lands/(?P<id>\d+)', [
            [ 'methods' => 'PUT',    'callback' => [ self::class, 'update_land' ], 'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'DELETE', 'callback' => [ self::class, 'delete_land' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );

        // ── Units ─────────────────────────────────────────────────
        register_rest_route( $ns, '/pc/projects/(?P<project_id>\d+)/units', [
            [ 'methods' => 'GET',  'callback' => [ self::class, 'get_units' ],   'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'POST', 'callback' => [ self::class, 'create_unit' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/units/(?P<id>\d+)', [
            [ 'methods' => 'PUT',    'callback' => [ self::class, 'update_unit' ], 'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'DELETE', 'callback' => [ self::class, 'delete_unit' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );

        // ── WBS ───────────────────────────────────────────────────
        register_rest_route( $ns, '/pc/projects/(?P<project_id>\d+)/wbs', [
            [ 'methods' => 'GET',  'callback' => [ self::class, 'get_wbs' ],   'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'POST', 'callback' => [ self::class, 'create_wbs' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/wbs/(?P<id>\d+)', [
            [ 'methods' => 'PUT',    'callback' => [ self::class, 'update_wbs' ], 'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'DELETE', 'callback' => [ self::class, 'delete_wbs' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );

        // ── Tasks ─────────────────────────────────────────────────
        register_rest_route( $ns, '/pc/projects/(?P<project_id>\d+)/tasks', [
            [ 'methods' => 'GET',  'callback' => [ self::class, 'get_tasks' ],   'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'POST', 'callback' => [ self::class, 'create_task' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/tasks/(?P<id>\d+)', [
            [ 'methods' => 'GET',    'callback' => [ self::class, 'get_task' ],    'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'PUT',    'callback' => [ self::class, 'update_task' ], 'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'DELETE', 'callback' => [ self::class, 'delete_task' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/tasks/(?P<id>\d+)/progress', [
            'methods' => 'POST', 'callback' => [ self::class, 'update_progress' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );
        register_rest_route( $ns, '/pc/tasks/(?P<id>\d+)/logs', [
            'methods' => 'GET', 'callback' => [ self::class, 'get_task_logs' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );

        // ── Dependencies ──────────────────────────────────────────
        register_rest_route( $ns, '/pc/projects/(?P<project_id>\d+)/dependencies', [
            [ 'methods' => 'GET',  'callback' => [ self::class, 'get_dependencies' ],   'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'POST', 'callback' => [ self::class, 'create_dependency' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/dependencies/(?P<id>\d+)', [
            'methods' => 'DELETE', 'callback' => [ self::class, 'delete_dependency' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );

        // ── Resources ─────────────────────────────────────────────
        register_rest_route( $ns, '/pc/projects/(?P<project_id>\d+)/resources', [
            [ 'methods' => 'GET',  'callback' => [ self::class, 'get_resources' ],   'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'POST', 'callback' => [ self::class, 'create_resource' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/resources/(?P<id>\d+)', [
            [ 'methods' => 'PUT',    'callback' => [ self::class, 'update_resource' ], 'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'DELETE', 'callback' => [ self::class, 'delete_resource' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );

        // ── Assignments ───────────────────────────────────────────
        register_rest_route( $ns, '/pc/projects/(?P<project_id>\d+)/assignments', [
            [ 'methods' => 'GET',  'callback' => [ self::class, 'get_assignments' ],   'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'POST', 'callback' => [ self::class, 'create_assignment' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/assignments/(?P<id>\d+)', [
            'methods' => 'DELETE', 'callback' => [ self::class, 'delete_assignment' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );

        // ── Members ───────────────────────────────────────────────
        register_rest_route( $ns, '/pc/projects/(?P<project_id>\d+)/members', [
            [ 'methods' => 'GET',  'callback' => [ self::class, 'get_members' ],   'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'POST', 'callback' => [ self::class, 'create_member' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/members/(?P<id>\d+)', [
            [ 'methods' => 'PUT',    'callback' => [ self::class, 'update_member' ], 'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'DELETE', 'callback' => [ self::class, 'delete_member' ], 'permission_callback' => 'acc_jwt_check_permission' ],
        ] );

        // ── Assignment confirm (→ Accounting event) ───────────────
        register_rest_route( $ns, '/pc/assignments/(?P<id>\d+)/confirm', [
            'methods' => 'POST', 'callback' => [ self::class, 'confirm_assignment' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );
        register_rest_route( $ns, '/pc/assignments/(?P<id>\d+)/unconfirm', [
            'methods' => 'POST', 'callback' => [ self::class, 'unconfirm_assignment' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );

        // ── Unit sale (→ Sale invoice) ─────────────────────────
        register_rest_route( $ns, '/pc/units/(?P<id>\d+)/sell', [
            'methods' => 'POST', 'callback' => [ self::class, 'sell_unit' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );

        // ── CPM Calculate ─────────────────────────────────────
        register_rest_route( $ns, '/pc/projects/(?P<id>\d+)/calculate-cpm', [
            'methods' => 'POST', 'callback' => [ self::class, 'calculate_cpm' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );

        // ── Project financials (cross-module dashboard) ────────
        register_rest_route( $ns, '/pc/projects/(?P<id>\d+)/financials', [
            'methods' => 'GET', 'callback' => [ self::class, 'get_financials' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );

        // ── Messages ──────────────────────────────────────────────
        register_rest_route( $ns, '/pc/projects/(?P<project_id>\d+)/messages', [
            [ 'methods' => 'GET',  'callback' => [ self::class, 'get_messages' ],   'permission_callback' => 'acc_jwt_check_permission' ],
            [ 'methods' => 'POST', 'callback' => [ self::class, 'send_message' ],   'permission_callback' => 'acc_jwt_check_permission' ],
        ] );
        register_rest_route( $ns, '/pc/messages/(?P<id>\d+)/read', [
            'methods' => 'POST', 'callback' => [ self::class, 'mark_read' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );
        register_rest_route( $ns, '/pc/messages/(?P<id>\d+)', [
            'methods' => 'DELETE', 'callback' => [ self::class, 'delete_message' ], 'permission_callback' => 'acc_jwt_check_permission',
        ] );
    }

    /* ═══════════════════════════════════════════════════════════
       PROJECTS
    ═══════════════════════════════════════════════════════════ */

    public static function get_projects( WP_REST_Request $req ): array {
        global $wpdb;

        $search = sanitize_text_field( $req->get_param('search') ?? '' );
        $status = sanitize_key( $req->get_param('status') ?? '' );

        $where = 'WHERE 1=1';
        $args  = [];
        if ( $search ) {
            $where .= ' AND (p.name LIKE %s OR p.code LIKE %s)';
            $args[] = "%{$search}%";
            $args[] = "%{$search}%";
        }
        if ( $status ) {
            $where .= ' AND p.status = %s';
            $args[] = $status;
        }

        $uid = acl_current_user_id();
        $pf  = acl_scope_sql_filter( $uid, 'project', 'p.id' );
        if ( $pf ) {
            if ( trim( $pf['sql'] ) === 'AND 1=0' ) {
                return [ 'success' => true, 'data' => [] ];
            }
            $where .= $pf['sql'];
            $args   = array_merge( $args, $pf['args'] );
        }

        $rows = PC_Service::list_projects( $where, $args );

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

    public static function get_project( WP_REST_Request $req ): WP_REST_Response {
        global $wpdb;
        $id  = (int) $req->get_param('id');
        $deny = self::_deny_project( $id, 'cpm.view' );
        if ( $deny ) return $deny;
        $row = $wpdb->get_row( $wpdb->prepare(
            "SELECT p.*, l.name AS land_name, cu.name AS client_name
             FROM {$wpdb->prefix}pc_projects p
             LEFT JOIN {$wpdb->prefix}pc_lands l ON l.id = p.land_id
             LEFT JOIN {$wpdb->prefix}ai_sale_customers cu ON cu.id = p.client_id
             WHERE p.id = %d", $id
        ), ARRAY_A );
        if ( ! $row ) return new WP_REST_Response( [ 'success' => false, 'message' => 'یافت نشد' ], 404 );
        return new WP_REST_Response( [ 'success' => true, 'data' => $row ], 200 );
    }

    public static function create_project( WP_REST_Request $req ): WP_REST_Response {
        $deny = acl_deny_if_scope_restricted( acl_current_user_id(), 'project', 'ایجاد پروژه فقط برای مدیران سراسری مجاز است' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->insert( $wpdb->prefix . 'pc_projects', [
            'code'         => sanitize_text_field( $req->get_param('code')         ?? '' ),
            'name'         => sanitize_text_field( $req->get_param('name') ),
            'description'  => sanitize_textarea_field( $req->get_param('description') ?? '' ),
            'type'         => sanitize_key( $req->get_param('type')    ?? 'construction' ),
            'status'       => sanitize_key( $req->get_param('status')  ?? 'planned' ),
            'start_date'   => $req->get_param('start_date') ?: null,
            'end_date'     => $req->get_param('end_date')   ?: null,
            'budget'       => (float) ( $req->get_param('budget') ?? 0 ),
            'land_id'      => $req->get_param('land_id')    ? (int) $req->get_param('land_id')    : null,
            'client_id'    => $req->get_param('client_id')  ? (int) $req->get_param('client_id')  : null,
            'manager_name' => sanitize_text_field( $req->get_param('manager_name') ?? '' ),
            'currency'     => sanitize_text_field( $req->get_param('currency') ?? 'IRR' ),
            'progress'     => 0.0,
        ] );
        return new WP_REST_Response( [ 'success' => true, 'id' => $wpdb->insert_id ], 201 );
    }

    public static function update_project( WP_REST_Request $req ): WP_REST_Response {
        global $wpdb;
        $id   = (int) $req->get_param('id');
        $deny = self::_deny_project( $id, 'cpm.view' );
        if ( $deny ) return $deny;
        $data = [];
        $map  = [
            'code' => 'text', 'name' => 'text', 'description' => 'textarea',
            'type' => 'key',  'status' => 'key', 'start_date' => 'raw',
            'end_date' => 'raw', 'budget' => 'float', 'land_id' => 'int',
            'client_id' => 'int', 'manager_name' => 'text', 'currency' => 'text', 'progress' => 'float',
        ];
        foreach ( $map as $param => $cast ) {
            if ( ! $req->has_param( $param ) ) continue;
            $v = $req->get_param( $param );
            $data[ $param ] = match( $cast ) {
                'text'     => sanitize_text_field( $v ),
                'textarea' => sanitize_textarea_field( $v ),
                'key'      => sanitize_key( $v ),
                'int'      => $v ? (int) $v : null,
                'float'    => (float) $v,
                default    => $v,
            };
        }
        if ( $data ) $wpdb->update( $wpdb->prefix . 'pc_projects', $data, [ 'id' => $id ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    public static function delete_project( WP_REST_Request $req ): WP_REST_Response {
        global $wpdb;
        $id = (int) $req->get_param('id');
        $deny = self::_deny_project( $id, 'cpm.view' );
        if ( $deny ) return $deny;
        $wpdb->delete( $wpdb->prefix . 'pc_tasks',             [ 'project_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_task_dependencies', [ 'project_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_task_logs',         [ 'project_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_wbs',               [ 'project_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_assignments',       [ 'project_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_resources',         [ 'project_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_members',           [ 'project_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_messages',          [ 'project_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_units',             [ 'project_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_projects',          [ 'id'         => $id ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    public static function project_dashboard( WP_REST_Request $req ): WP_REST_Response {
        $id = (int) $req->get_param('id');
        $deny = self::_deny_project( $id, 'cpm.view' );
        if ( $deny ) return $deny;

        $ws = PC_Service::workspace( $id, 'dashboard' );
        if ( empty( $ws['project'] ) ) {
            return new WP_REST_Response( [ 'success' => false, 'message' => 'یافت نشد' ], 404 );
        }

        return new WP_REST_Response( [
            'success'      => true,
            'project'      => $ws['project'],
            'tasks'        => $ws['tasks'] ?? [],
            'wbs'          => $ws['wbs'] ?? [],
            'dependencies' => $ws['dependencies'] ?? [],
            'members'      => $ws['members'] ?? [],
            'resources'    => $ws['resources'] ?? [],
            'assignments'  => $ws['assignments'] ?? [],
            'stats'        => $ws['stats'] ?? [],
        ], 200 );
    }

    /**
     * GET /pc/projects/{id}/workspace?scope=tasks|gantt|resources|dashboard|full
     * بارگذاری یکپارچه داده‌های پروژه (کاهش round-trip فرانت)
     */
    public static function get_workspace( WP_REST_Request $req ): WP_REST_Response {
        $id = (int) $req->get_param('id');
        $deny = self::_deny_project( $id, 'cpm.view' );
        if ( $deny ) return $deny;

        $scope = sanitize_key( $req->get_param('scope') ?? 'full' );
        if ( ! in_array( $scope, [ 'tasks', 'gantt', 'resources', 'dashboard', 'full' ], true ) ) {
            $scope = 'full';
        }

        $ws = PC_Service::workspace( $id, $scope );
        if ( in_array( $scope, [ 'gantt', 'dashboard', 'full' ], true ) && empty( $ws['project'] ) ) {
            return new WP_REST_Response( [ 'success' => false, 'message' => 'یافت نشد' ], 404 );
        }

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

    /* ═══════════════════════════════════════════════════════════
       LANDS
    ═══════════════════════════════════════════════════════════ */

    public static function get_lands( WP_REST_Request $req ): array {
        global $wpdb;
        $search = sanitize_text_field( $req->get_param('search') ?? '' );
        $where  = $search ? $wpdb->prepare( 'WHERE name LIKE %s OR code LIKE %s', "%{$search}%", "%{$search}%" ) : '';
        $rows   = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}pc_lands {$where} ORDER BY id DESC", ARRAY_A );
        return [ 'success' => true, 'data' => $rows ?: [] ];
    }

    public static function create_land( WP_REST_Request $req ): WP_REST_Response {
        global $wpdb;
        $wpdb->insert( $wpdb->prefix . 'pc_lands', [
            'code'        => sanitize_text_field( $req->get_param('code')        ?? '' ),
            'name'        => sanitize_text_field( $req->get_param('name') ),
            'address'     => sanitize_textarea_field( $req->get_param('address') ?? '' ),
            'area_sqm'    => (float) ( $req->get_param('area_sqm') ?? 0 ),
            'price'       => (float) ( $req->get_param('price')    ?? 0 ),
            'status'      => sanitize_key( $req->get_param('status') ?? 'available' ),
            'description' => sanitize_textarea_field( $req->get_param('description') ?? '' ),
        ] );
        return new WP_REST_Response( [ 'success' => true, 'id' => $wpdb->insert_id ], 201 );
    }

    public static function update_land( WP_REST_Request $req ): WP_REST_Response {
        global $wpdb;
        $id   = (int) $req->get_param('id');
        $data = [];
        if ( $req->has_param('code') )        $data['code']        = sanitize_text_field( $req->get_param('code') );
        if ( $req->has_param('name') )        $data['name']        = sanitize_text_field( $req->get_param('name') );
        if ( $req->has_param('address') )     $data['address']     = sanitize_textarea_field( $req->get_param('address') );
        if ( $req->has_param('area_sqm') )    $data['area_sqm']    = (float) $req->get_param('area_sqm');
        if ( $req->has_param('price') )       $data['price']       = (float) $req->get_param('price');
        if ( $req->has_param('status') )      $data['status']      = sanitize_key( $req->get_param('status') );
        if ( $req->has_param('description') ) $data['description'] = sanitize_textarea_field( $req->get_param('description') );
        if ( $data ) $wpdb->update( $wpdb->prefix . 'pc_lands', $data, [ 'id' => $id ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

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

    /* ═══════════════════════════════════════════════════════════
       UNITS
    ═══════════════════════════════════════════════════════════ */

    public static function get_units( WP_REST_Request $req ): array {
        $pid  = (int) $req->get_param('project_id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return [ 'success' => false, 'message' => $deny->get_data()['message'] ?? 'دسترسی مجاز نیست', 'data' => [] ];
        global $wpdb;
        $rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT u.*, c.name AS buyer_name FROM {$wpdb->prefix}pc_units u
             LEFT JOIN {$wpdb->prefix}ai_sale_customers c ON c.id = u.buyer_customer_id
             WHERE u.project_id = %d ORDER BY u.floor ASC, u.code ASC", $pid
        ), ARRAY_A );
        return [ 'success' => true, 'data' => $rows ?: [] ];
    }

    public static function create_unit( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project( (int) $req->get_param('project_id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->insert( $wpdb->prefix . 'pc_units', [
            'project_id' => (int) $req->get_param('project_id'),
            'land_id'    => $req->get_param('land_id') ? (int) $req->get_param('land_id') : null,
            'code'       => sanitize_text_field( $req->get_param('code')  ?? '' ),
            'name'       => sanitize_text_field( $req->get_param('name') ),
            'type'       => sanitize_key( $req->get_param('type')    ?? 'residential' ),
            'floor'      => $req->get_param('floor') !== null ? (int) $req->get_param('floor') : null,
            'area_sqm'   => (float) ( $req->get_param('area_sqm') ?? 0 ),
            'status'     => sanitize_key( $req->get_param('status') ?? 'available' ),
            'price'      => (float) ( $req->get_param('price')     ?? 0 ),
            'buyer_customer_id' => $req->get_param('buyer_customer_id') ? (int) $req->get_param('buyer_customer_id') : null,
        ] );
        return new WP_REST_Response( [ 'success' => true, 'id' => $wpdb->insert_id ], 201 );
    }

    public static function update_unit( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_units', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id   = (int) $req->get_param('id');
        $data = [];
        if ( $req->has_param('name') )               $data['name']               = sanitize_text_field( $req->get_param('name') );
        if ( $req->has_param('code') )               $data['code']               = sanitize_text_field( $req->get_param('code') );
        if ( $req->has_param('type') )               $data['type']               = sanitize_key( $req->get_param('type') );
        if ( $req->has_param('floor') )              $data['floor']              = (int) $req->get_param('floor');
        if ( $req->has_param('area_sqm') )           $data['area_sqm']           = (float) $req->get_param('area_sqm');
        if ( $req->has_param('status') )             $data['status']             = sanitize_key( $req->get_param('status') );
        if ( $req->has_param('price') )              $data['price']              = (float) $req->get_param('price');
        if ( $req->has_param('buyer_customer_id') )  $data['buyer_customer_id']  = $req->get_param('buyer_customer_id') ? (int) $req->get_param('buyer_customer_id') : null;
        if ( $data ) $wpdb->update( $wpdb->prefix . 'pc_units', $data, [ 'id' => $id ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    public static function delete_unit( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_units', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->delete( $wpdb->prefix . 'pc_units', [ 'id' => (int) $req->get_param('id') ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    /* ═══════════════════════════════════════════════════════════
       WBS
    ═══════════════════════════════════════════════════════════ */

    public static function get_wbs( WP_REST_Request $req ): array {
        $pid  = (int) $req->get_param('project_id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return [ 'success' => false, 'message' => $deny->get_data()['message'] ?? 'دسترسی مجاز نیست', 'data' => [] ];
        global $wpdb;
        $rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}pc_wbs WHERE project_id=%d ORDER BY sort_index ASC",
            $pid
        ), ARRAY_A );
        return [ 'success' => true, 'data' => $rows ?: [] ];
    }

    public static function create_wbs( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project( (int) $req->get_param('project_id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $pid     = (int) $req->get_param('project_id');
        $max_idx = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(MAX(sort_index),0) FROM {$wpdb->prefix}pc_wbs WHERE project_id=%d", $pid ) );
        $wpdb->insert( $wpdb->prefix . 'pc_wbs', [
            'project_id'  => $pid,
            'parent_id'   => $req->get_param('parent_id') ? (int) $req->get_param('parent_id') : null,
            'code'        => sanitize_text_field( $req->get_param('code')        ?? '' ),
            'name'        => sanitize_text_field( $req->get_param('name') ),
            'description' => sanitize_textarea_field( $req->get_param('description') ?? '' ),
            'sort_index'  => $max_idx + 1,
        ] );
        return new WP_REST_Response( [ 'success' => true, 'id' => $wpdb->insert_id ], 201 );
    }

    public static function update_wbs( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_wbs', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id   = (int) $req->get_param('id');
        $data = [];
        if ( $req->has_param('name') )       $data['name']       = sanitize_text_field( $req->get_param('name') );
        if ( $req->has_param('code') )       $data['code']       = sanitize_text_field( $req->get_param('code') );
        if ( $req->has_param('parent_id') )  $data['parent_id']  = $req->get_param('parent_id') ? (int) $req->get_param('parent_id') : null;
        if ( $data ) $wpdb->update( $wpdb->prefix . 'pc_wbs', $data, [ 'id' => $id ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    public static function delete_wbs( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_wbs', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id = (int) $req->get_param('id');
        $wpdb->update( $wpdb->prefix . 'pc_tasks', [ 'wbs_id' => null ], [ 'wbs_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_wbs', [ 'id' => $id ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    /* ═══════════════════════════════════════════════════════════
       TASKS
    ═══════════════════════════════════════════════════════════ */

    public static function get_tasks( WP_REST_Request $req ): array {
        $pid  = (int) $req->get_param('project_id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return [ 'success' => false, 'message' => $deny->get_data()['message'] ?? 'دسترسی مجاز نیست', 'data' => [] ];
        global $wpdb;
        $status = sanitize_key( $req->get_param('status') ?? '' );
        $where  = $status ? $wpdb->prepare( 'AND t.status = %s', $status ) : '';
        $rows   = $wpdb->get_results( $wpdb->prepare(
            "SELECT t.*, w.name AS wbs_name, m.name AS assignee_name
             FROM {$wpdb->prefix}pc_tasks t
             LEFT JOIN {$wpdb->prefix}pc_wbs     w ON w.id = t.wbs_id
             LEFT JOIN {$wpdb->prefix}pc_members m ON m.id = t.assigned_member_id
             WHERE t.project_id = %d {$where}
             ORDER BY t.sort_index ASC, t.id ASC",
            $pid
        ), ARRAY_A );
        return [ 'success' => true, 'data' => $rows ?: [] ];
    }

    public static function get_task( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_tasks', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id  = (int) $req->get_param('id');
        $row = $wpdb->get_row( $wpdb->prepare(
            "SELECT t.*, w.name AS wbs_name, m.name AS assignee_name
             FROM {$wpdb->prefix}pc_tasks t
             LEFT JOIN {$wpdb->prefix}pc_wbs     w ON w.id = t.wbs_id
             LEFT JOIN {$wpdb->prefix}pc_members m ON m.id = t.assigned_member_id
             WHERE t.id = %d", $id
        ), ARRAY_A );
        if ( ! $row ) return new WP_REST_Response( [ 'success' => false, 'message' => 'یافت نشد' ], 404 );
        return new WP_REST_Response( [ 'success' => true, 'data' => $row ], 200 );
    }

    public static function create_task( WP_REST_Request $req ): WP_REST_Response {
        $pid  = (int) $req->get_param('project_id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $max_idx = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(MAX(sort_index),0) FROM {$wpdb->prefix}pc_tasks WHERE project_id=%d", $pid ) );
        $wpdb->insert( $wpdb->prefix . 'pc_tasks', [
            'project_id'         => $pid,
            'wbs_id'             => $req->get_param('wbs_id')             ? (int)   $req->get_param('wbs_id')             : null,
            'code'               => sanitize_text_field( $req->get_param('code')        ?? '' ),
            'name'               => sanitize_text_field( $req->get_param('name') ),
            'description'        => sanitize_textarea_field( $req->get_param('description') ?? '' ),
            'type'               => sanitize_key( $req->get_param('type')          ?? 'task' ),
            'duration'           => (float) ( $req->get_param('duration')          ?? 0 ),
            'duration_unit'      => sanitize_key( $req->get_param('duration_unit') ?? 'day' ),
            'start_date'         => $req->get_param('start_date') ?: null,
            'end_date'           => $req->get_param('end_date')   ?: null,
            'status'             => sanitize_key( $req->get_param('status')        ?? 'not_started' ),
            'priority'           => sanitize_key( $req->get_param('priority')      ?? 'medium' ),
            'progress'           => (float) ( $req->get_param('progress')          ?? 0 ),
            'assigned_member_id' => $req->get_param('assigned_member_id') ? (int) $req->get_param('assigned_member_id') : null,
            'sort_index'         => $max_idx + 1,
        ] );
        return new WP_REST_Response( [ 'success' => true, 'id' => $wpdb->insert_id ], 201 );
    }

    public static function update_task( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_tasks', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id   = (int) $req->get_param('id');
        $data = [];
        $map  = [
            'wbs_id' => 'int', 'code' => 'text', 'name' => 'text', 'description' => 'textarea',
            'type' => 'key', 'duration' => 'float', 'duration_unit' => 'key',
            'start_date' => 'raw', 'end_date' => 'raw', 'status' => 'key',
            'priority' => 'key', 'progress' => 'float', 'assigned_member_id' => 'int',
            'is_critical' => 'int', 'total_float' => 'float', 'free_float' => 'float',
            'early_start' => 'raw', 'early_finish' => 'raw', 'late_start' => 'raw', 'late_finish' => 'raw',
        ];
        foreach ( $map as $param => $cast ) {
            if ( ! $req->has_param( $param ) ) continue;
            $v = $req->get_param( $param );
            $data[ $param ] = match( $cast ) {
                'text'     => sanitize_text_field( $v ),
                'textarea' => sanitize_textarea_field( $v ),
                'key'      => sanitize_key( $v ),
                'int'      => $v !== null && $v !== '' ? (int) $v : null,
                'float'    => (float) $v,
                default    => $v,
            };
        }
        if ( $data ) $wpdb->update( $wpdb->prefix . 'pc_tasks', $data, [ 'id' => $id ] );

        // Auto-update project progress
        $pid = (int) $wpdb->get_var( $wpdb->prepare( "SELECT project_id FROM {$wpdb->prefix}pc_tasks WHERE id=%d", $id ) );
        if ( $pid ) self::_recalc_project_progress( $pid );

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

    public static function update_progress( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_tasks', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id           = (int) $req->get_param('id');
        $new_progress = min( 100, max( 0, (float) $req->get_param('progress') ) );
        $new_status   = sanitize_key( $req->get_param('status') ?? '' );
        $note         = sanitize_textarea_field( $req->get_param('note') ?? '' );
        $reporter     = sanitize_text_field( $req->get_param('reporter_name') ?? '' );

        $task = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pc_tasks WHERE id=%d", $id ), ARRAY_A );
        if ( ! $task ) return new WP_REST_Response( [ 'success' => false, 'message' => 'یافت نشد' ], 404 );

        $update = [ 'progress' => $new_progress ];
        if ( $new_status ) $update['status'] = $new_status;
        elseif ( $new_progress >= 100 ) $update['status'] = 'completed';
        elseif ( $new_progress > 0 )    $update['status'] = 'in_progress';

        $wpdb->update( $wpdb->prefix . 'pc_tasks', $update, [ 'id' => $id ] );

        $wpdb->insert( $wpdb->prefix . 'pc_task_logs', [
            'task_id'       => $id,
            'project_id'    => $task['project_id'],
            'reporter_name' => $reporter,
            'old_progress'  => $task['progress'],
            'new_progress'  => $new_progress,
            'old_status'    => $task['status'],
            'new_status'    => $update['status'] ?? $task['status'],
            'note'          => $note,
        ] );

        self::_recalc_project_progress( (int) $task['project_id'] );

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

    public static function delete_task( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_tasks', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id = (int) $req->get_param('id');
        $wpdb->delete( $wpdb->prefix . 'pc_task_dependencies', [ 'predecessor_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_task_dependencies', [ 'successor_id'   => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_assignments',       [ 'task_id'        => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_task_logs',         [ 'task_id'        => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_tasks',             [ 'id'             => $id ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    public static function get_task_logs( WP_REST_Request $req ): array {
        $deny = self::_deny_project_entity( 'pc_tasks', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return [ 'success' => false, 'message' => $deny->get_data()['message'] ?? 'دسترسی مجاز نیست', 'data' => [] ];
        global $wpdb;
        $id   = (int) $req->get_param('id');
        $rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}pc_task_logs WHERE task_id=%d ORDER BY created_at DESC LIMIT 50", $id
        ), ARRAY_A );
        return [ 'success' => true, 'data' => $rows ?: [] ];
    }

    /* ═══════════════════════════════════════════════════════════
       DEPENDENCIES
    ═══════════════════════════════════════════════════════════ */

    public static function get_dependencies( WP_REST_Request $req ): array {
        $pid  = (int) $req->get_param('project_id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return [ 'success' => false, 'message' => $deny->get_data()['message'] ?? 'دسترسی مجاز نیست', 'data' => [] ];
        global $wpdb;
        $rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT d.*, p.name AS predecessor_name, s.name AS successor_name
             FROM {$wpdb->prefix}pc_task_dependencies d
             JOIN {$wpdb->prefix}pc_tasks p ON p.id = d.predecessor_id
             JOIN {$wpdb->prefix}pc_tasks s ON s.id = d.successor_id
             WHERE d.project_id = %d", $pid
        ), ARRAY_A );
        return [ 'success' => true, 'data' => $rows ?: [] ];
    }

    public static function create_dependency( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project( (int) $req->get_param('project_id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->replace( $wpdb->prefix . 'pc_task_dependencies', [
            'project_id'     => (int) $req->get_param('project_id'),
            'predecessor_id' => (int) $req->get_param('predecessor_id'),
            'successor_id'   => (int) $req->get_param('successor_id'),
            'type'           => strtoupper( sanitize_key( $req->get_param('type') ?? 'FS' ) ),
            'lag'            => (float) ( $req->get_param('lag')      ?? 0 ),
            'lag_unit'       => sanitize_key( $req->get_param('lag_unit') ?? 'day' ),
        ] );
        return new WP_REST_Response( [ 'success' => true, 'id' => $wpdb->insert_id ], 201 );
    }

    public static function delete_dependency( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_task_dependencies', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->delete( $wpdb->prefix . 'pc_task_dependencies', [ 'id' => (int) $req->get_param('id') ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    /* ═══════════════════════════════════════════════════════════
       RESOURCES
    ═══════════════════════════════════════════════════════════ */

    public static function get_resources( WP_REST_Request $req ): array {
        $pid  = (int) $req->get_param('project_id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return [ 'success' => false, 'message' => $deny->get_data()['message'] ?? 'دسترسی مجاز نیست', 'data' => [] ];
        global $wpdb;
        $rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}pc_resources WHERE project_id=%d ORDER BY type ASC, name ASC",
            $pid
        ), ARRAY_A );
        return [ 'success' => true, 'data' => $rows ?: [] ];
    }

    public static function create_resource( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project( (int) $req->get_param('project_id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->insert( $wpdb->prefix . 'pc_resources', [
            'project_id'    => (int) $req->get_param('project_id'),
            'code'          => sanitize_text_field( $req->get_param('code')  ?? '' ),
            'name'          => sanitize_text_field( $req->get_param('name') ),
            'type'          => sanitize_key( $req->get_param('type')      ?? 'human' ),
            'unit'          => sanitize_text_field( $req->get_param('unit')  ?? '' ),
            'cost_per_unit' => (float) ( $req->get_param('cost_per_unit') ?? 0 ),
            'currency'      => sanitize_text_field( $req->get_param('currency') ?? 'IRR' ),
            'is_active'     => 1,
        ] );
        return new WP_REST_Response( [ 'success' => true, 'id' => $wpdb->insert_id ], 201 );
    }

    public static function update_resource( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_resources', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id   = (int) $req->get_param('id');
        $data = [];
        if ( $req->has_param('name') )          $data['name']          = sanitize_text_field( $req->get_param('name') );
        if ( $req->has_param('code') )          $data['code']          = sanitize_text_field( $req->get_param('code') );
        if ( $req->has_param('type') )          $data['type']          = sanitize_key( $req->get_param('type') );
        if ( $req->has_param('unit') )          $data['unit']          = sanitize_text_field( $req->get_param('unit') );
        if ( $req->has_param('cost_per_unit') ) $data['cost_per_unit'] = (float) $req->get_param('cost_per_unit');
        if ( $req->has_param('is_active') )     $data['is_active']     = (int) $req->get_param('is_active');
        if ( $data ) $wpdb->update( $wpdb->prefix . 'pc_resources', $data, [ 'id' => $id ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    public static function delete_resource( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_resources', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id = (int) $req->get_param('id');
        $wpdb->delete( $wpdb->prefix . 'pc_assignments', [ 'resource_id' => $id ] );
        $wpdb->delete( $wpdb->prefix . 'pc_resources',   [ 'id'          => $id ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    /* ═══════════════════════════════════════════════════════════
       ASSIGNMENTS
    ═══════════════════════════════════════════════════════════ */

    public static function get_assignments( WP_REST_Request $req ): array {
        $pid  = (int) $req->get_param('project_id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return [ 'success' => false, 'message' => $deny->get_data()['message'] ?? 'دسترسی مجاز نیست', 'data' => [] ];
        global $wpdb;
        $rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT a.*, t.name AS task_name, r.name AS resource_name, r.type AS resource_type, r.unit
             FROM {$wpdb->prefix}pc_assignments a
             JOIN {$wpdb->prefix}pc_tasks     t ON t.id = a.task_id
             JOIN {$wpdb->prefix}pc_resources r ON r.id = a.resource_id
             WHERE a.project_id = %d ORDER BY t.sort_index ASC",
            $pid
        ), ARRAY_A );
        return [ 'success' => true, 'data' => $rows ?: [] ];
    }

    public static function create_assignment( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project( (int) $req->get_param('project_id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $qty        = (float) ( $req->get_param('quantity')      ?? 0 );
        $cpu        = (float) ( $req->get_param('cost_per_unit') ?? 0 );
        $wpdb->insert( $wpdb->prefix . 'pc_assignments', [
            'project_id'    => (int) $req->get_param('project_id'),
            'task_id'       => (int) $req->get_param('task_id'),
            'resource_id'   => (int) $req->get_param('resource_id'),
            'quantity'      => $qty,
            'cost_per_unit' => $cpu,
            'total_cost'    => $qty * $cpu,
            'notes'         => sanitize_textarea_field( $req->get_param('notes') ?? '' ),
        ] );
        return new WP_REST_Response( [ 'success' => true, 'id' => $wpdb->insert_id ], 201 );
    }

    public static function delete_assignment( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_assignments', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->delete( $wpdb->prefix . 'pc_assignments', [ 'id' => (int) $req->get_param('id') ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    /* ═══════════════════════════════════════════════════════════
       MEMBERS
    ═══════════════════════════════════════════════════════════ */

    public static function get_members( WP_REST_Request $req ): array {
        $pid  = (int) $req->get_param('project_id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return [ 'success' => false, 'message' => $deny->get_data()['message'] ?? 'دسترسی مجاز نیست', 'data' => [] ];
        global $wpdb;
        $rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}pc_members WHERE project_id=%d ORDER BY role ASC, name ASC", $pid
        ), ARRAY_A );
        return [ 'success' => true, 'data' => $rows ?: [] ];
    }

    public static function create_member( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project( (int) $req->get_param('project_id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->insert( $wpdb->prefix . 'pc_members', [
            'project_id' => (int) $req->get_param('project_id'),
            'wp_user_id' => $req->get_param('wp_user_id') ? (int) $req->get_param('wp_user_id') : null,
            'name'       => sanitize_text_field( $req->get_param('name') ),
            'email'      => sanitize_email( $req->get_param('email')      ?? '' ),
            'phone'      => sanitize_text_field( $req->get_param('phone') ?? '' ),
            'role'       => sanitize_key( $req->get_param('role')         ?? 'engineer' ),
            'joined_at'  => $req->get_param('joined_at') ?: date('Y-m-d'),
            'is_active'  => 1,
        ] );
        return new WP_REST_Response( [ 'success' => true, 'id' => $wpdb->insert_id ], 201 );
    }

    public static function update_member( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_members', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id   = (int) $req->get_param('id');
        $data = [];
        if ( $req->has_param('name') )      $data['name']      = sanitize_text_field( $req->get_param('name') );
        if ( $req->has_param('email') )     $data['email']     = sanitize_email( $req->get_param('email') );
        if ( $req->has_param('phone') )     $data['phone']     = sanitize_text_field( $req->get_param('phone') );
        if ( $req->has_param('role') )      $data['role']      = sanitize_key( $req->get_param('role') );
        if ( $req->has_param('is_active') ) $data['is_active'] = (int) $req->get_param('is_active');
        if ( $data ) $wpdb->update( $wpdb->prefix . 'pc_members', $data, [ 'id' => $id ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    public static function delete_member( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_members', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->delete( $wpdb->prefix . 'pc_members', [ 'id' => (int) $req->get_param('id') ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    /* ═══════════════════════════════════════════════════════════
       MESSAGES
    ═══════════════════════════════════════════════════════════ */

    public static function get_messages( WP_REST_Request $req ): array {
        $pid  = (int) $req->get_param('project_id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return [ 'success' => false, 'message' => $deny->get_data()['message'] ?? 'دسترسی مجاز نیست', 'data' => [] ];
        global $wpdb;
        $receiver = $req->get_param('receiver_member_id') ? (int) $req->get_param('receiver_member_id') : null;

        if ( $receiver ) {
            $sender = (int) ( $req->get_param('sender_member_id') ?? 0 );
            $rows   = $wpdb->get_results( $wpdb->prepare(
                "SELECT m.*, s.name AS sender_name, r.name AS receiver_name
                 FROM {$wpdb->prefix}pc_messages m
                 JOIN {$wpdb->prefix}pc_members s ON s.id = m.sender_member_id
                 LEFT JOIN {$wpdb->prefix}pc_members r ON r.id = m.receiver_member_id
                 WHERE m.project_id = %d
                   AND ((m.sender_member_id=%d AND m.receiver_member_id=%d)
                        OR (m.sender_member_id=%d AND m.receiver_member_id=%d)
                        OR m.receiver_member_id IS NULL)
                 ORDER BY m.created_at ASC LIMIT 200",
                $pid, $sender, $receiver, $receiver, $sender
            ), ARRAY_A );
        } else {
            $rows = $wpdb->get_results( $wpdb->prepare(
                "SELECT m.*, s.name AS sender_name, r.name AS receiver_name
                 FROM {$wpdb->prefix}pc_messages m
                 JOIN {$wpdb->prefix}pc_members s ON s.id = m.sender_member_id
                 LEFT JOIN {$wpdb->prefix}pc_members r ON r.id = m.receiver_member_id
                 WHERE m.project_id = %d
                 ORDER BY m.created_at ASC LIMIT 200", $pid
            ), ARRAY_A );
        }
        return [ 'success' => true, 'data' => $rows ?: [] ];
    }

    public static function send_message( WP_REST_Request $req ): WP_REST_Response {
        $pid  = (int) $req->get_param('project_id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $sender  = (int) $req->get_param('sender_member_id');
        $message = sanitize_textarea_field( $req->get_param('message') );
        if ( ! $sender || ! $message ) {
            return new WP_REST_Response( [ 'success' => false, 'message' => 'sender_member_id و message الزامی‌اند' ], 400 );
        }
        $wpdb->insert( $wpdb->prefix . 'pc_messages', [
            'project_id'         => $pid,
            'sender_member_id'   => $sender,
            'receiver_member_id' => $req->get_param('receiver_member_id') ? (int) $req->get_param('receiver_member_id') : null,
            'message'            => $message,
            'parent_message_id'  => $req->get_param('parent_message_id') ? (int) $req->get_param('parent_message_id') : null,
            'is_read'            => 0,
        ] );
        $id  = $wpdb->insert_id;
        $row = $wpdb->get_row( $wpdb->prepare(
            "SELECT m.*, s.name AS sender_name FROM {$wpdb->prefix}pc_messages m
             JOIN {$wpdb->prefix}pc_members s ON s.id = m.sender_member_id
             WHERE m.id = %d", $id
        ), ARRAY_A );
        return new WP_REST_Response( [ 'success' => true, 'data' => $row ], 201 );
    }

    public static function mark_read( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_messages', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->update( $wpdb->prefix . 'pc_messages', [ 'is_read' => 1 ], [ 'id' => (int) $req->get_param('id') ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    public static function delete_message( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_messages', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $wpdb->delete( $wpdb->prefix . 'pc_messages', [ 'id' => (int) $req->get_param('id') ] );
        return new WP_REST_Response( [ 'success' => true ], 200 );
    }

    /* ═══════════════════════════════════════════════════════════
       INTEGRATION — ACCOUNTING
    ═══════════════════════════════════════════════════════════ */

    /**
     * تأیید تخصیص منبع → ثبت رویداد حسابداری PROJECT_COST
     * POST /pc/assignments/{id}/confirm
     */
    public static function confirm_assignment( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_assignments', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id = (int) $req->get_param('id');

        $a = $wpdb->get_row( $wpdb->prepare(
            "SELECT a.*, r.name AS resource_name, r.type AS resource_type,
                    t.name AS task_name, p.name AS project_name
             FROM {$wpdb->prefix}pc_assignments a
             JOIN {$wpdb->prefix}pc_resources r ON r.id = a.resource_id
             JOIN {$wpdb->prefix}pc_tasks     t ON t.id = a.task_id
             JOIN {$wpdb->prefix}pc_projects  p ON p.id = a.project_id
             WHERE a.id = %d", $id
        ), ARRAY_A );

        if ( ! $a ) return new WP_REST_Response( [ 'success' => false, 'message' => 'تخصیص یافت نشد' ], 404 );
        if ( $a['is_confirmed'] ) return new WP_REST_Response( [ 'success' => false, 'message' => 'قبلاً تأیید شده' ], 409 );

        $meta = wp_json_encode( [
            'assignment_id' => $id,
            'project_id'    => (int) $a['project_id'],
            'project_name'  => $a['project_name'],
            'task_name'     => $a['task_name'],
            'resource_name' => $a['resource_name'],
            'resource_type' => $a['resource_type'],
            'quantity'      => (float) $a['quantity'],
            'cost_per_unit' => (float) $a['cost_per_unit'],
            'total_cost'    => (float) $a['total_cost'],
        ] );

        $wpdb->insert( $wpdb->prefix . 'ai_acc_events', [
            'event_type'     => 'PROJECT_COST',
            'workflow_state' => 'pending',
            'amount'         => (float) $a['total_cost'],
            'metadata'       => $meta,
            'reference_type' => 'pc_assignment',
            'reference_id'   => $id,
            'description'    => "هزینه پروژه: {$a['project_name']} — {$a['task_name']} — {$a['resource_name']}",
        ] );
        $event_id = (int) $wpdb->insert_id;

        $wpdb->update( $wpdb->prefix . 'pc_assignments', [
            'is_confirmed' => 1,
            'acc_event_id' => $event_id,
        ], [ 'id' => $id ] );

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

    /**
     * لغو تأیید تخصیص
     * POST /pc/assignments/{id}/unconfirm
     */
    public static function unconfirm_assignment( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_assignments', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id = (int) $req->get_param('id');
        $a  = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pc_assignments WHERE id=%d", $id ), ARRAY_A );
        if ( ! $a ) return new WP_REST_Response( [ 'success' => false, 'message' => 'یافت نشد' ], 404 );

        // Remove accounting event if still pending
        if ( $a['acc_event_id'] ) {
            $state = $wpdb->get_var( $wpdb->prepare(
                "SELECT workflow_state FROM {$wpdb->prefix}ai_acc_events WHERE id=%d", $a['acc_event_id']
            ) );
            if ( $state === 'pending' ) {
                $wpdb->delete( $wpdb->prefix . 'ai_acc_events', [ 'id' => $a['acc_event_id'] ] );
            }
        }

        $wpdb->update( $wpdb->prefix . 'pc_assignments', [
            'is_confirmed' => 0,
            'acc_event_id' => null,
        ], [ 'id' => $id ] );

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

    /* ═══════════════════════════════════════════════════════════
       INTEGRATION — SALES (فروش واحد → فاکتور)
    ═══════════════════════════════════════════════════════════ */

    /**
     * فروش واحد → ایجاد فاکتور فروش در ماژول فروش + ارسال رویداد حسابداری
     * POST /pc/units/{id}/sell
     * Body: { customer_id, price, note }
     */
    public static function sell_unit( WP_REST_Request $req ): WP_REST_Response {
        $deny = self::_deny_project_entity( 'pc_units', (int) $req->get_param('id'), 'cpm.view' );
        if ( $deny ) return $deny;
        global $wpdb;
        $id          = (int) $req->get_param('id');
        $customer_id = (int) ( $req->get_param('customer_id') ?? 0 );
        $price       = (float) ( $req->get_param('price')       ?? 0 );
        $note        = sanitize_textarea_field( $req->get_param('note') ?? '' );

        $unit = $wpdb->get_row( $wpdb->prepare(
            "SELECT u.*, p.name AS project_name, p.currency
             FROM {$wpdb->prefix}pc_units u
             JOIN {$wpdb->prefix}pc_projects p ON p.id = u.project_id
             WHERE u.id = %d", $id
        ), ARRAY_A );

        if ( ! $unit ) return new WP_REST_Response( [ 'success' => false, 'message' => 'واحد یافت نشد' ], 404 );
        if ( $unit['status'] === 'sold' ) return new WP_REST_Response( [ 'success' => false, 'message' => 'واحد قبلاً فروخته شده است' ], 409 );
        if ( ! $customer_id ) return new WP_REST_Response( [ 'success' => false, 'message' => 'customer_id الزامی است' ], 400 );

        $sale_price = $price > 0 ? $price : (float) $unit['price'];

        // ① Create sale invoice
        $year   = date('Y');
        $seq    = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}ai_sale_invoices WHERE invoice_no LIKE 'INV-{$year}-%'" );
        $inv_no = sprintf( 'INV-%s-%04d', $year, $seq + 1 );

        $unit_label = trim( ( $unit['code'] ? $unit['code'] . ' — ' : '' ) . $unit['name'] );
        $description = "فروش واحد: {$unit_label} | پروژه: {$unit['project_name']}" . ( $note ? " | {$note}" : '' );

        $wpdb->insert( $wpdb->prefix . 'ai_sale_invoices', [
            'invoice_no'    => $inv_no,
            'customer_id'   => $customer_id,
            'invoice_date'  => date('Y-m-d'),
            'status'        => 'finalized',
            'subtotal'      => $sale_price,
            'discount_amount' => 0,
            'net_amount'    => $sale_price,
            'vat_amount'    => 0,
            'total_amount'  => $sale_price,
            'description'   => $description,
        ] );
        $invoice_id = (int) $wpdb->insert_id;

        // ② Add invoice item (unit as product line)
        $wpdb->insert( $wpdb->prefix . 'ai_sale_invoice_items', [
            'invoice_id'    => $invoice_id,
            'product_id'    => null,
            'description'   => $unit_label,
            'quantity'      => 1,
            'unit_price'    => $sale_price,
            'discount_pct'  => 0,
            'discount_amt'  => 0,
            'line_total'    => $sale_price,
            'sort_order'    => 1,
        ] );

        // ③ Post accounting event SALE_INVOICE
        $meta = wp_json_encode( [
            'invoice_id'   => $invoice_id,
            'invoice_no'   => $inv_no,
            'unit_id'      => $id,
            'unit_name'    => $unit_label,
            'project_id'   => (int) $unit['project_id'],
            'project_name' => $unit['project_name'],
            'customer_id'  => $customer_id,
            'total_amount' => $sale_price,
        ] );
        $wpdb->insert( $wpdb->prefix . 'ai_acc_events', [
            'event_type'     => 'SALE_INVOICE',
            'workflow_state' => 'pending',
            'amount'         => $sale_price,
            'metadata'       => $meta,
            'reference_type' => 'pc_unit_sale',
            'reference_id'   => $id,
            'description'    => "فروش واحد: {$unit_label}",
        ] );

        // ④ Update unit status + link invoice
        $wpdb->update( $wpdb->prefix . 'pc_units', [
            'status'             => 'sold',
            'buyer_customer_id'  => $customer_id,
            'sale_invoice_id'    => $invoice_id,
            'price'              => $sale_price,
        ], [ 'id' => $id ] );

        return new WP_REST_Response( [
            'success'    => true,
            'invoice_id' => $invoice_id,
            'invoice_no' => $inv_no,
        ], 201 );
    }

    /* ═══════════════════════════════════════════════════════════
       CPM CALCULATE
    ═══════════════════════════════════════════════════════════ */

    /**
     * POST /pc/projects/{id}/calculate-cpm
     * محاسبه الگوریتم CPM و ذخیره نتایج در DB
     */
    public static function calculate_cpm( WP_REST_Request $req ): WP_REST_Response {
        global $wpdb;
        $pid = (int) $req->get_param('id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return $deny;

        $project = $wpdb->get_row( $wpdb->prepare(
            "SELECT id, start_date FROM {$wpdb->prefix}pc_projects WHERE id=%d", $pid
        ), ARRAY_A );
        if ( ! $project ) return new WP_REST_Response( [ 'success' => false, 'message' => 'پروژه یافت نشد' ], 404 );

        $tasks = $wpdb->get_results( $wpdb->prepare(
            "SELECT id, duration, duration_unit FROM {$wpdb->prefix}pc_tasks WHERE project_id=%d", $pid
        ), ARRAY_A );

        if ( empty( $tasks ) ) {
            return new WP_REST_Response( [ 'success' => true, 'data' => [], 'count' => 0 ], 200 );
        }

        $deps = $wpdb->get_results( $wpdb->prepare(
            "SELECT predecessor_id, successor_id, type, lag FROM {$wpdb->prefix}pc_task_dependencies WHERE project_id=%d", $pid
        ), ARRAY_A );

        $start_date = $project['start_date'] ?: date( 'Y-m-d' );
        $results    = CPM_Calculator::calculate( $start_date, $tasks, $deps );
        $updated    = PC_Service::apply_cpm_results( $pid, $results );

        return new WP_REST_Response( [
            'success' => true,
            'data'    => array_values( $results ),
            'count'   => count( $results ),
            'updated' => $updated,
        ], 200 );
    }

    /* ═══════════════════════════════════════════════════════════
       INTEGRATION — FINANCIALS (داشبورد مالی یکپارچه)
    ═══════════════════════════════════════════════════════════ */

    /**
     * GET /pc/projects/{id}/financials
     * گزارش مالی کامل پروژه از همه ماژول‌ها
     */
    public static function get_financials( WP_REST_Request $req ): WP_REST_Response {
        global $wpdb;
        $pid = (int) $req->get_param('id');
        $deny = self::_deny_project( $pid, 'cpm.view' );
        if ( $deny ) return $deny;

        $project = $wpdb->get_row( $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}pc_projects WHERE id=%d", $pid
        ), ARRAY_A );
        if ( ! $project ) return new WP_REST_Response( [ 'success' => false, 'message' => 'پروژه یافت نشد' ], 404 );

        // ── ۱. هزینه تخصیص منابع ──────────────────────────────
        $assignment_costs = $wpdb->get_results( $wpdb->prepare(
            "SELECT a.id, a.task_id, a.resource_id, a.quantity, a.cost_per_unit, a.total_cost,
                    a.is_confirmed, a.acc_event_id,
                    r.name AS resource_name, r.type AS resource_type, r.unit AS resource_unit,
                    t.name AS task_name, t.wbs_id,
                    w.name AS wbs_name
             FROM {$wpdb->prefix}pc_assignments a
             JOIN {$wpdb->prefix}pc_resources r ON r.id = a.resource_id
             JOIN {$wpdb->prefix}pc_tasks     t ON t.id = a.task_id
             LEFT JOIN {$wpdb->prefix}pc_wbs  w ON w.id = t.wbs_id
             WHERE a.project_id = %d
             ORDER BY t.sort_index ASC", $pid
        ), ARRAY_A );

        $total_assignment_cost           = array_sum( array_column( $assignment_costs, 'total_cost' ) );
        $total_assignment_cost_confirmed = array_sum(
            array_column( array_filter( $assignment_costs, fn($a) => $a['is_confirmed'] ), 'total_cost' )
        );

        // ── ۲. فاکتورهای خرید مرتبط ──────────────────────────
        $purchase_invoices = $wpdb->get_results( $wpdb->prepare(
            "SELECT i.id, i.invoice_no, i.invoice_date, i.status,
                    i.total_amount, i.net_amount, i.vat_amount,
                    s.name AS supplier_name
             FROM {$wpdb->prefix}ai_purchase_invoices i
             LEFT JOIN {$wpdb->prefix}ai_purchase_suppliers s ON s.id = i.supplier_id
             WHERE i.project_id = %d
             ORDER BY i.invoice_date DESC", $pid
        ), ARRAY_A );

        $total_purchase = array_sum( array_column(
            array_filter( $purchase_invoices, fn($i) => $i['status'] === 'finalized' ),
            'total_amount'
        ) );

        // ── ۳. هزینه حقوق مرتبط ──────────────────────────────
        $payroll_periods = $wpdb->get_results( $wpdb->prepare(
            "SELECT pr.id, pr.period_name, pr.period_code, pr.status,
                    pr.total_gross, pr.total_net, pr.total_deductions
             FROM {$wpdb->prefix}ai_payroll_periods pr
             WHERE pr.project_id = %d
             ORDER BY pr.year DESC, pr.month DESC", $pid
        ), ARRAY_A );

        $total_payroll = array_sum( array_column(
            array_filter( $payroll_periods, fn($p) => in_array( $p['status'], [ 'finalized', 'paid' ], true ) ),
            'total_net'
        ) );

        // ── ۴. درآمد فروش واحدها ─────────────────────────────
        $sold_units = $wpdb->get_results( $wpdb->prepare(
            "SELECT u.id, u.name, u.code, u.floor, u.area_sqm, u.price,
                    u.sale_invoice_id, u.buyer_customer_id,
                    c.name AS buyer_name,
                    si.invoice_no, si.invoice_date, si.total_amount AS invoice_amount
             FROM {$wpdb->prefix}pc_units u
             LEFT JOIN {$wpdb->prefix}ai_sale_customers c  ON c.id  = u.buyer_customer_id
             LEFT JOIN {$wpdb->prefix}ai_sale_invoices  si ON si.id = u.sale_invoice_id
             WHERE u.project_id = %d AND u.status = 'sold'
             ORDER BY si.invoice_date DESC", $pid
        ), ARRAY_A );

        $total_revenue = array_sum( array_column( $sold_units, 'invoice_amount' ) );

        // ── ۵. رویدادهای حسابداری مرتبط ─────────────────────
        $acc_events = $wpdb->get_results( $wpdb->prepare(
            "SELECT id, event_type, workflow_state, amount, description, created_at
             FROM {$wpdb->prefix}ai_acc_events
             WHERE reference_type IN ('pc_assignment','pc_unit_sale')
               AND reference_id IN (
                   SELECT id FROM {$wpdb->prefix}pc_assignments WHERE project_id=%d
                   UNION
                   SELECT id FROM {$wpdb->prefix}pc_units WHERE project_id=%d
               )
             ORDER BY created_at DESC LIMIT 50", $pid, $pid
        ), ARRAY_A );

        // ── خلاصه مالی ───────────────────────────────────────
        $budget        = (float) $project['budget'];
        $total_cost    = $total_assignment_cost_confirmed + $total_purchase + $total_payroll;
        $budget_used   = $budget > 0 ? round( $total_cost / $budget * 100, 1 ) : 0;
        $net_pl        = $total_revenue - $total_cost;

        return new WP_REST_Response( [
            'success'          => true,
            'project'          => $project,
            'assignment_costs' => $assignment_costs,
            'purchase_invoices'=> $purchase_invoices,
            'payroll_periods'  => $payroll_periods,
            'sold_units'       => $sold_units,
            'acc_events'       => $acc_events,
            'summary' => [
                'budget'                     => $budget,
                'total_assignment_cost'      => $total_assignment_cost,
                'total_assignment_confirmed' => $total_assignment_cost_confirmed,
                'total_purchase'             => $total_purchase,
                'total_payroll'              => $total_payroll,
                'total_cost'                 => $total_cost,
                'total_revenue'              => $total_revenue,
                'net_pl'                     => $net_pl,
                'budget_used_pct'            => $budget_used,
            ],
        ], 200 );
    }

    /* ═══════════════════════════════════════════════════════════
       PRIVATE HELPERS
    ═══════════════════════════════════════════════════════════ */

    private static function _deny_project( int $project_id, string $perm = '' ): ?WP_REST_Response {
        $uid = acl_current_user_id();
        $key = $uid . ':' . $project_id . ':' . $perm;
        if ( array_key_exists( $key, self::$_access_cache ) ) {
            return self::$_access_cache[ $key ];
        }
        $deny = acl_deny_scope( $uid, 'project', $project_id ?: null, $perm );
        self::$_access_cache[ $key ] = $deny;
        return $deny;
    }

    private static function _deny_project_entity( string $table, int $entity_id, string $perm = '' ): ?WP_REST_Response {
        global $wpdb;
        $pid = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT project_id FROM {$wpdb->prefix}{$table} WHERE id = %d", $entity_id
        ) );
        if ( ! $pid ) return new WP_REST_Response( [ 'success' => false, 'message' => 'یافت نشد' ], 404 );
        return self::_deny_project( $pid, $perm );
    }

    private static function _recalc_project_progress( int $pid ): void {
        global $wpdb;
        $avg = (float) $wpdb->get_var( $wpdb->prepare(
            "SELECT COALESCE(AVG(progress),0) FROM {$wpdb->prefix}pc_tasks WHERE project_id=%d AND type!='summary'", $pid
        ) );
        $wpdb->update( $wpdb->prefix . 'pc_projects', [ 'progress' => round( $avg, 1 ) ], [ 'id' => $pid ] );
    }
}
