<?php
/**
 * svc-activity-cost.php — حسابداری صنعتی / بهای تمام‌شده فعالیت‌ها (ABC)
 * + گراف تخصیص: سفارش ↔ فعالیت ↔ نفر
 */
if ( ! defined( 'ABSPATH' ) ) exit;

class Svc_Activity_Cost {

    /** کاتالوگ پیش‌فرض فعالیت‌های میدانی HVAC */
    public static function default_activities(): array {
        return [
            [
                'code'        => 'diagnose',
                'name'        => 'عیب‌یابی و تشخیص',
                'pool'        => 'labor',
                'weight'      => 15.0,
                'roles'       => [ 'lead', 'senior', 'assistant' ],
                'driver'      => 'weight',
                'sort'        => 10,
            ],
            [
                'code'        => 'travel',
                'name'        => 'ایاب و ذهاب / حضور در محل',
                'pool'        => 'labor',
                'weight'      => 10.0,
                'roles'       => [ 'lead', 'assistant', 'helper' ],
                'driver'      => 'hours',
                'sort'        => 20,
            ],
            [
                'code'        => 'repair_labor',
                'name'        => 'اجرای تعمیر / سرویس',
                'pool'        => 'labor',
                'weight'      => 45.0,
                'roles'       => [ 'lead', 'assistant' ],
                'driver'      => 'hours',
                'sort'        => 30,
            ],
            [
                'code'        => 'parts_install',
                'name'        => 'نصب و مصرف قطعات',
                'pool'        => 'parts',
                'weight'      => 100.0,
                'roles'       => [ 'lead', 'assistant' ],
                'driver'      => 'weight',
                'sort'        => 40,
            ],
            [
                'code'        => 'quality_check',
                'name'        => 'کنترل کیفیت و تحویل',
                'pool'        => 'labor',
                'weight'      => 15.0,
                'roles'       => [ 'lead', 'senior' ],
                'driver'      => 'weight',
                'sort'        => 50,
            ],
            [
                'code'        => 'overhead',
                'name'        => 'سربار انرژی و تعمیرگاه',
                'pool'        => 'overhead',
                'weight'      => 100.0,
                'roles'       => [ 'company' ],
                'driver'      => 'fixed',
                'sort'        => 60,
            ],
            [
                'code'        => 'supervision',
                'name'        => 'نظارت ارشد',
                'pool'        => 'senior',
                'weight'      => 100.0,
                'roles'       => [ 'senior' ],
                'driver'      => 'weight',
                'sort'        => 70,
            ],
        ];
    }

    public static function role_labels(): array {
        return [
            'lead'      => 'کارشناس مسئول',
            'assistant' => 'کمک‌کارشناس',
            'senior'    => 'کارشناس ارشد',
            'helper'    => 'کمک میدانی',
            'company'   => 'شرکت / سربار',
        ];
    }

    public static function ensure_tables(): void {
        global $wpdb;
        $pre = $wpdb->prefix;
        $charset = 'DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci';

        $wpdb->query( "CREATE TABLE IF NOT EXISTS {$pre}svc_acc_activities (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            code VARCHAR(40) NOT NULL,
            name VARCHAR(160) NOT NULL,
            pool VARCHAR(30) NOT NULL DEFAULT 'labor',
            weight DECIMAL(8,2) NOT NULL DEFAULT 0,
            roles_json JSON NULL,
            driver VARCHAR(20) NOT NULL DEFAULT 'weight',
            sort_order INT NOT NULL DEFAULT 0,
            is_active TINYINT(1) NOT NULL DEFAULT 1,
            updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            UNIQUE KEY code (code)
        ) {$charset}" );

        $wpdb->query( "CREATE TABLE IF NOT EXISTS {$pre}svc_acc_order_crew (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            request_id BIGINT UNSIGNED NOT NULL,
            report_id BIGINT UNSIGNED NULL,
            user_id BIGINT UNSIGNED NOT NULL,
            role_code VARCHAR(30) NOT NULL DEFAULT 'lead',
            weight DECIMAL(8,2) NOT NULL DEFAULT 1,
            hours DECIMAL(8,2) NOT NULL DEFAULT 0,
            note VARCHAR(255) NOT NULL DEFAULT '',
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            KEY request_id (request_id),
            KEY report_id (report_id),
            KEY user_id (user_id)
        ) {$charset}" );

        $wpdb->query( "CREATE TABLE IF NOT EXISTS {$pre}svc_acc_activity_costs (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            report_id BIGINT UNSIGNED NOT NULL,
            request_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            activity_code VARCHAR(40) NOT NULL,
            activity_name VARCHAR(160) NOT NULL DEFAULT '',
            pool VARCHAR(30) NOT NULL DEFAULT 'labor',
            person_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            person_label VARCHAR(200) NOT NULL DEFAULT '',
            role_code VARCHAR(30) NOT NULL DEFAULT '',
            driver_qty DECIMAL(12,4) NOT NULL DEFAULT 0,
            share_pct DECIMAL(8,4) NOT NULL DEFAULT 0,
            amount DECIMAL(15,2) NOT NULL DEFAULT 0,
            revenue_share DECIMAL(15,2) NOT NULL DEFAULT 0,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            KEY report_id (report_id),
            KEY person (person_id),
            KEY activity (activity_code)
        ) {$charset}" );

        self::_seed_activities_if_empty();
    }

    private static function _seed_activities_if_empty(): void {
        global $wpdb;
        $t = $wpdb->prefix . 'svc_acc_activities';
        $n = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$t}" );
        if ( $n > 0 ) return;
        foreach ( self::default_activities() as $a ) {
            $wpdb->insert( $t, [
                'code'       => $a['code'],
                'name'       => $a['name'],
                'pool'       => $a['pool'],
                'weight'     => $a['weight'],
                'roles_json' => wp_json_encode( $a['roles'], JSON_UNESCAPED_UNICODE ),
                'driver'     => $a['driver'],
                'sort_order' => $a['sort'],
                'is_active'  => 1,
            ] );
        }
    }

    public static function list_activities( bool $active_only = true ): array {
        global $wpdb;
        self::ensure_tables();
        $t = $wpdb->prefix . 'svc_acc_activities';
        $sql = "SELECT * FROM {$t}" . ( $active_only ? ' WHERE is_active = 1' : '' ) . ' ORDER BY sort_order ASC, id ASC';
        $rows = $wpdb->get_results( $sql, ARRAY_A ) ?: [];
        return array_map( [ self::class, '_format_activity' ], $rows );
    }

    public static function save_activities( array $items ): array {
        global $wpdb;
        self::ensure_tables();
        $t = $wpdb->prefix . 'svc_acc_activities';
        $saved = 0;
        foreach ( $items as $item ) {
            $code = sanitize_key( (string) ( $item['code'] ?? '' ) );
            if ( ! $code ) continue;
            $roles = $item['roles'] ?? [];
            if ( is_string( $roles ) ) {
                $roles = array_filter( array_map( 'trim', explode( ',', $roles ) ) );
            }
            $data = [
                'name'       => sanitize_text_field( (string) ( $item['name'] ?? $code ) ),
                'pool'       => sanitize_key( (string) ( $item['pool'] ?? 'labor' ) ),
                'weight'     => (float) ( $item['weight'] ?? 0 ),
                'roles_json' => wp_json_encode( array_values( $roles ), JSON_UNESCAPED_UNICODE ),
                'driver'     => sanitize_key( (string) ( $item['driver'] ?? 'weight' ) ),
                'sort_order' => (int) ( $item['sort'] ?? $item['sort_order'] ?? 0 ),
                'is_active'  => empty( $item['is_active'] ) && isset( $item['is_active'] ) ? 0 : 1,
            ];
            $id = (int) ( $item['id'] ?? 0 );
            $ex = $id ?: (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$t} WHERE code=%s", $code ) );
            if ( $ex ) {
                $wpdb->update( $t, $data, [ 'id' => $ex ] );
            } else {
                $data['code'] = $code;
                $wpdb->insert( $t, $data );
            }
            $saved++;
        }
        return [ 'success' => true, 'saved' => $saved, 'data' => self::list_activities( false ) ];
    }

    private static function _format_activity( array $row ): array {
        $roles = json_decode( $row['roles_json'] ?? '[]', true ) ?: [];
        return [
            'id'     => (int) $row['id'],
            'code'   => $row['code'],
            'name'   => $row['name'],
            'pool'   => $row['pool'],
            'weight' => (float) $row['weight'],
            'roles'  => $roles,
            'driver' => $row['driver'],
            'sort'   => (int) $row['sort_order'],
            'is_active' => (int) $row['is_active'] === 1,
        ];
    }

    /** خدمه سفارش — اگر خالی باشد از تکنسین/ارشد گزارش ساخته می‌شود */
    public static function get_crew( int $request_id, ?int $report_id = null ): array {
        global $wpdb;
        self::ensure_tables();
        $t = $wpdb->prefix . 'svc_acc_order_crew';
        $rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT c.*, u.display_name
             FROM {$t} c
             LEFT JOIN {$wpdb->users} u ON u.ID = c.user_id
             WHERE c.request_id = %d
             ORDER BY c.id ASC",
            $request_id
        ), ARRAY_A ) ?: [];

        if ( $rows ) {
            return array_map( static function( $r ) {
                return [
                    'id'        => (int) $r['id'],
                    'request_id'=> (int) $r['request_id'],
                    'report_id' => (int) ( $r['report_id'] ?? 0 ),
                    'user_id'   => (int) $r['user_id'],
                    'name'      => $r['display_name'] ?: ( 'کاربر #' . $r['user_id'] ),
                    'role'      => $r['role_code'],
                    'weight'    => (float) $r['weight'],
                    'hours'     => (float) $r['hours'],
                    'note'      => $r['note'] ?? '',
                ];
            }, $rows );
        }

        return self::_infer_crew_from_assignment( $request_id, $report_id );
    }

    private static function _infer_crew_from_assignment( int $request_id, ?int $report_id ): array {
        global $wpdb;
        $req = $wpdb->get_row( $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}svc_requests WHERE id = %d", $request_id
        ), ARRAY_A );
        if ( ! $req ) return [];

        $tech_id = 0;
        if ( $report_id ) {
            $tech_id = (int) $wpdb->get_var( $wpdb->prepare(
                "SELECT technician_id FROM {$wpdb->prefix}svc_reports WHERE id = %d", $report_id
            ) );
        }
        if ( ! $tech_id ) {
            $tech_id = (int) ( $req['assigned_technician_id'] ?? 0 );
        }
        $crew = [];
        if ( $tech_id ) {
            $name = (string) $wpdb->get_var( $wpdb->prepare(
                "SELECT display_name FROM {$wpdb->users} WHERE ID = %d", $tech_id
            ) );
            $crew[] = [
                'id' => 0, 'request_id' => $request_id, 'report_id' => (int) $report_id,
                'user_id' => $tech_id, 'name' => $name ?: "کاربر #{$tech_id}",
                'role' => 'lead', 'weight' => 1.0, 'hours' => 4.0, 'note' => 'خودکار از انتساب',
                'inferred' => true,
            ];
        }
        if ( $tech_id && file_exists( ACC_PATH . 'includes/helpers/svc-teams.php' ) ) {
            require_once ACC_PATH . 'includes/helpers/svc-teams.php';
            $senior_id = (int) ( Svc_Teams::senior_of_technician( $tech_id ) ?: 0 );
            if ( $senior_id ) {
                $sname = (string) $wpdb->get_var( $wpdb->prepare(
                    "SELECT display_name FROM {$wpdb->users} WHERE ID = %d", $senior_id
                ) );
                $crew[] = [
                    'id' => 0, 'request_id' => $request_id, 'report_id' => (int) $report_id,
                    'user_id' => $senior_id, 'name' => $sname ?: "کاربر #{$senior_id}",
                    'role' => 'senior', 'weight' => 0.5, 'hours' => 1.0, 'note' => 'خودکار از تیم',
                    'inferred' => true,
                ];
            }
        }
        return $crew;
    }

    public static function save_crew( int $request_id, array $members, ?int $report_id = null ): array {
        global $wpdb;
        self::ensure_tables();
        $t = $wpdb->prefix . 'svc_acc_order_crew';
        $wpdb->delete( $t, [ 'request_id' => $request_id ] );

        $n = 0;
        foreach ( $members as $m ) {
            $uid = (int) ( $m['user_id'] ?? 0 );
            if ( $uid <= 0 ) continue;
            $wpdb->insert( $t, [
                'request_id' => $request_id,
                'report_id'  => $report_id ?: null,
                'user_id'    => $uid,
                'role_code'  => sanitize_key( (string) ( $m['role'] ?? 'lead' ) ),
                'weight'     => max( 0.01, (float) ( $m['weight'] ?? 1 ) ),
                'hours'      => max( 0, (float) ( $m['hours'] ?? 0 ) ),
                'note'       => sanitize_text_field( (string) ( $m['note'] ?? '' ) ),
            ] );
            $n++;
        }
        return [ 'success' => true, 'saved' => $n, 'data' => self::get_crew( $request_id, $report_id ) ];
    }

    /**
     * تخصیص ABC: استخرهای هزینه → فعالیت‌ها → نفرها
     */
    public static function allocate( array $report, ?array $splits = null ): array {
        if ( ! class_exists( 'Svc_Accounting' ) ) {
            require_once ACC_PATH . 'includes/helpers/svc-accounting.php';
        }
        $splits = $splits ?: Svc_Accounting::calculate_splits( $report );
        $costs  = $splits['costs'] ?? [];
        $amounts = $splits['amounts'] ?? [];

        $pools = [
            'labor'    => (float) ( $costs['tech_fee'] ?? 0 ),
            'senior'   => (float) ( $costs['senior_fee'] ?? 0 ),
            'parts'    => (float) ( $costs['parts_cogs'] ?? 0 ) + (float) ( $costs['supplier_cost'] ?? 0 ),
            'overhead' => (float) ( $costs['overhead_total'] ?? 0 ),
        ];

        $request_id = (int) ( $report['request_id'] ?? $splits['entities']['request_id'] ?? 0 );
        $report_id  = (int) ( $report['id'] ?? 0 );
        $crew = $request_id ? self::get_crew( $request_id, $report_id ?: null ) : [];
        $activities = self::list_activities( true );

        $lines = [];
        $person_totals = [];

        foreach ( $activities as $act ) {
            $pool_key = $act['pool'];
            $pool_amt = (float) ( $pools[ $pool_key ] ?? 0 );
            if ( $pool_amt <= 0 ) continue;

            /* وزن نرمال داخل استخر */
            $pool_weight_sum = 0.0;
            foreach ( $activities as $a2 ) {
                if ( $a2['pool'] === $pool_key ) $pool_weight_sum += (float) $a2['weight'];
            }
            if ( $pool_weight_sum <= 0 ) continue;
            $activity_amount = round( $pool_amt * ( (float) $act['weight'] / $pool_weight_sum ), 2 );
            if ( $activity_amount <= 0 ) continue;

            $eligible = array_values( array_filter( $crew, static function( $m ) use ( $act ) {
                return in_array( $m['role'], $act['roles'], true );
            } ) );

            /* سربار شرکت */
            if ( in_array( 'company', $act['roles'], true ) && empty( $eligible ) ) {
                $lines[] = [
                    'activity_code' => $act['code'],
                    'activity_name' => $act['name'],
                    'pool'          => $pool_key,
                    'person_id'     => 0,
                    'person_label'  => 'شرکت / سربار',
                    'role_code'     => 'company',
                    'driver_qty'    => 1,
                    'share_pct'     => 100,
                    'amount'        => $activity_amount,
                    'revenue_share' => 0,
                ];
                continue;
            }

            if ( empty( $eligible ) ) {
                /* فقط برای فعالیت‌های عمومی labor/parts به lead برگرد؛ نقش‌های محدود را به شرکت بده */
                $strict = array_intersect( $act['roles'], [ 'senior', 'company' ] );
                if ( $strict && ! array_intersect( $act['roles'], [ 'lead', 'assistant', 'helper' ] ) ) {
                    $lines[] = [
                        'activity_code' => $act['code'],
                        'activity_name' => $act['name'],
                        'pool'          => $pool_key,
                        'person_id'     => 0,
                        'person_label'  => 'شرکت / تخصیص‌نشده',
                        'role_code'     => 'company',
                        'driver_qty'    => 1,
                        'share_pct'     => 100,
                        'amount'        => $activity_amount,
                        'revenue_share' => 0,
                    ];
                    continue;
                }
                $eligible = array_values( array_filter( $crew, static fn( $m ) => ( $m['role'] ?? '' ) === 'lead' ) );
                if ( empty( $eligible ) ) {
                    $eligible = $crew ?: [[
                        'user_id' => (int) ( $splits['entities']['technician_id'] ?? 0 ),
                        'name'    => 'کارشناس',
                        'role'    => 'lead',
                        'weight'  => 1,
                        'hours'   => 1,
                    ]];
                }
                $eligible = array_values( array_filter( $eligible, static fn( $m ) => (int) ( $m['user_id'] ?? 0 ) > 0 ) );
                if ( empty( $eligible ) ) continue;
            }

            $driver = $act['driver'];
            $den = 0.0;
            foreach ( $eligible as $m ) {
                $den += $driver === 'hours'
                    ? max( 0.01, (float) $m['hours'] )
                    : max( 0.01, (float) $m['weight'] );
            }
            if ( $den <= 0 ) $den = 1;

            $allocated = 0.0;
            $last_i = count( $eligible ) - 1;
            foreach ( $eligible as $i => $m ) {
                $qty = $driver === 'hours'
                    ? max( 0.01, (float) $m['hours'] )
                    : max( 0.01, (float) $m['weight'] );
                $pct = round( 100 * $qty / $den, 4 );
                $amt = ( $i === $last_i )
                    ? round( $activity_amount - $allocated, 2 )
                    : round( $activity_amount * $qty / $den, 2 );
                $allocated += $amt;

                $pid = (int) ( $m['user_id'] ?? 0 );
                $plabel = $m['name'] ?? ( $pid ? "کاربر #{$pid}" : '—' );
                $lines[] = [
                    'activity_code' => $act['code'],
                    'activity_name' => $act['name'],
                    'pool'          => $pool_key,
                    'person_id'     => $pid,
                    'person_label'  => $plabel,
                    'role_code'     => $m['role'] ?? '',
                    'driver_qty'    => $qty,
                    'share_pct'     => $pct,
                    'amount'        => $amt,
                    'revenue_share' => 0,
                ];

                if ( $pid > 0 ) {
                    if ( ! isset( $person_totals[ $pid ] ) ) {
                        $person_totals[ $pid ] = [
                            'person_id' => $pid,
                            'name'      => $plabel,
                            'role'      => $m['role'] ?? '',
                            'amount'    => 0,
                            'hours'     => (float) ( $m['hours'] ?? 0 ),
                            'by_activity' => [],
                        ];
                    }
                    $person_totals[ $pid ]['amount'] += $amt;
                    $person_totals[ $pid ]['by_activity'][ $act['code'] ] =
                        ( $person_totals[ $pid ]['by_activity'][ $act['code'] ] ?? 0 ) + $amt;
                }
            }
        }

        /* سهم درآمدی نسبی نفرها از خالص خدمات */
        $labor_rev = (float) ( $amounts['net_services'] ?? 0 );
        $person_sum = array_sum( array_column( $person_totals, 'amount' ) );
        if ( $labor_rev > 0 && $person_sum > 0 ) {
            foreach ( $person_totals as &$pt ) {
                $pt['revenue_share'] = round( $labor_rev * ( $pt['amount'] / $person_sum ), 2 );
            }
            unset( $pt );
            foreach ( $lines as &$ln ) {
                if ( $ln['person_id'] > 0 && $person_sum > 0 ) {
                    $ln['revenue_share'] = round( $labor_rev * ( $ln['amount'] / $person_sum ), 2 );
                }
            }
            unset( $ln );
        }

        return [
            'report_id'     => $report_id,
            'request_id'    => $request_id,
            'pools'         => $pools,
            'crew'          => $crew,
            'activities'    => $activities,
            'lines'         => $lines,
            'person_shares' => array_values( $person_totals ),
            'totals'        => [
                'activity_cost' => array_sum( array_column( $lines, 'amount' ) ),
                'persons'       => count( $person_totals ),
                'revenue'       => (float) ( $amounts['total'] ?? 0 ),
                'margin'        => (float) ( $costs['gross_margin'] ?? 0 ),
            ],
            'splits'        => $splits,
        ];
    }

    public static function persist_allocation( int $report_id, array $allocation ): void {
        global $wpdb;
        self::ensure_tables();
        $t = $wpdb->prefix . 'svc_acc_activity_costs';
        $wpdb->delete( $t, [ 'report_id' => $report_id ] );
        $request_id = (int) ( $allocation['request_id'] ?? 0 );
        foreach ( $allocation['lines'] ?? [] as $ln ) {
            $wpdb->insert( $t, [
                'report_id'      => $report_id,
                'request_id'     => $request_id,
                'activity_code'  => $ln['activity_code'],
                'activity_name'  => $ln['activity_name'],
                'pool'           => $ln['pool'],
                'person_id'      => (int) $ln['person_id'],
                'person_label'   => $ln['person_label'],
                'role_code'      => $ln['role_code'],
                'driver_qty'     => $ln['driver_qty'],
                'share_pct'      => $ln['share_pct'],
                'amount'         => $ln['amount'],
                'revenue_share'  => $ln['revenue_share'] ?? 0,
            ] );
        }
    }

    /** گراف حسابداری صنعتی: Order / Activity / Person */
    public static function cost_graph( array $filters = [] ): array {
        global $wpdb;
        self::ensure_tables();
        $pre = $wpdb->prefix;
        $from = $filters['date_from'] ?? date( 'Y-m-01' );
        $to   = $filters['date_to'] ?? date( 'Y-m-d' );
        $report_id = (int) ( $filters['report_id'] ?? 0 );

        if ( $report_id > 0 ) {
            $report = $wpdb->get_row( $wpdb->prepare(
                "SELECT * FROM {$pre}svc_reports WHERE id = %d", $report_id
            ), ARRAY_A );
            if ( ! $report ) {
                return [ 'nodes' => [], 'edges' => [], 'person_shares' => [], 'orders' => [] ];
            }
            $alloc = self::allocate( $report );
            return self::_graph_from_allocations( [ $alloc ], $report );
        }

        $reports = $wpdb->get_results( $wpdb->prepare(
            "SELECT * FROM {$pre}svc_reports
             WHERE status = 'final' AND finalized_at BETWEEN %s AND %s
             ORDER BY finalized_at DESC LIMIT 40",
            $from . ' 00:00:00', $to . ' 23:59:59'
        ), ARRAY_A ) ?: [];

        $allocs = [];
        foreach ( $reports as $r ) {
            $saved = $wpdb->get_results( $wpdb->prepare(
                "SELECT * FROM {$pre}svc_acc_activity_costs WHERE report_id = %d", (int) $r['id']
            ), ARRAY_A );
            if ( $saved ) {
                $person_shares = [];
                $lines = [];
                foreach ( $saved as $s ) {
                    $lines[] = [
                        'activity_code' => $s['activity_code'],
                        'activity_name' => $s['activity_name'],
                        'pool' => $s['pool'],
                        'person_id' => (int) $s['person_id'],
                        'person_label' => $s['person_label'],
                        'role_code' => $s['role_code'],
                        'driver_qty' => (float) $s['driver_qty'],
                        'share_pct' => (float) $s['share_pct'],
                        'amount' => (float) $s['amount'],
                        'revenue_share' => (float) $s['revenue_share'],
                    ];
                    $pid = (int) $s['person_id'];
                    if ( $pid > 0 ) {
                        if ( ! isset( $person_shares[ $pid ] ) ) {
                            $person_shares[ $pid ] = [
                                'person_id' => $pid,
                                'name' => $s['person_label'],
                                'role' => $s['role_code'],
                                'amount' => 0,
                                'by_activity' => [],
                            ];
                        }
                        $person_shares[ $pid ]['amount'] += (float) $s['amount'];
                        $person_shares[ $pid ]['by_activity'][ $s['activity_code'] ] =
                            ( $person_shares[ $pid ]['by_activity'][ $s['activity_code'] ] ?? 0 ) + (float) $s['amount'];
                    }
                }
                $allocs[] = [
                    'report_id' => (int) $r['id'],
                    'request_id' => (int) $r['request_id'],
                    'lines' => $lines,
                    'person_shares' => array_values( $person_shares ),
                    'totals' => [
                        'activity_cost' => array_sum( array_column( $lines, 'amount' ) ),
                        'revenue' => (float) $r['total'],
                    ],
                    'tracking' => $r['tracking_code'] ?? ( 'SRD-' . $r['id'] ),
                    'customer' => $r['customer_name'] ?? '',
                ];
            } else {
                $a = self::allocate( $r );
                $a['tracking'] = $r['tracking_code'] ?? ( 'SRD-' . $r['id'] );
                $a['customer'] = $r['customer_name'] ?? '';
                $allocs[] = $a;
            }
        }

        return self::_graph_from_allocations( $allocs );
    }

    private static function _graph_from_allocations( array $allocs, ?array $single_report = null ): array {
        $nodes = [];
        $edges = [];
        $add_node = static function( string $id, string $type, string $label, float $value = 0 ) use ( &$nodes ) {
            if ( ! isset( $nodes[ $id ] ) ) {
                $nodes[ $id ] = [
                    'id'    => $id,
                    'type'  => $type,
                    'label' => $label,
                    'value' => 0.0,
                ];
            }
            $nodes[ $id ]['value'] += $value;
        };

        $person_agg = [];
        $orders = [];

        foreach ( $allocs as $alloc ) {
            $rid = (int) ( $alloc['report_id'] ?? 0 );
            $oid = 'order:' . $rid;
            $track = $alloc['tracking'] ?? ( $single_report['tracking_code'] ?? ( 'SRD-' . $rid ) );
            $cust  = $alloc['customer'] ?? ( $single_report['customer_name'] ?? '' );
            $rev   = (float) ( $alloc['totals']['revenue'] ?? $single_report['total'] ?? 0 );
            $add_node( $oid, 'order', $track . ( $cust ? " — {$cust}" : '' ), $rev );
            $orders[] = [
                'report_id'  => $rid,
                'tracking'   => $track,
                'customer'   => $cust,
                'revenue'    => $rev,
                'cost'       => (float) ( $alloc['totals']['activity_cost'] ?? 0 ),
                'persons'    => count( $alloc['person_shares'] ?? [] ),
            ];

            foreach ( $alloc['lines'] ?? [] as $ln ) {
                $aid = 'act:' . $ln['activity_code'];
                $add_node( $aid, 'activity', $ln['activity_name'], (float) $ln['amount'] );
                $ek1 = $oid . '>' . $aid;
                if ( ! isset( $edges[ $ek1 ] ) ) {
                    $edges[ $ek1 ] = [
                        'from' => $oid, 'to' => $aid,
                        'amount' => 0, 'label' => $ln['pool'],
                    ];
                }
                $edges[ $ek1 ]['amount'] += (float) $ln['amount'];

                $pid = (int) $ln['person_id'];
                $nid = $pid > 0 ? ( 'person:' . $pid ) : 'person:company';
                $add_node( $nid, 'person', $ln['person_label'], (float) $ln['amount'] );
                $ek2 = $aid . '>' . $nid;
                if ( ! isset( $edges[ $ek2 ] ) ) {
                    $edges[ $ek2 ] = [
                        'from' => $aid, 'to' => $nid,
                        'amount' => 0, 'label' => ( $ln['share_pct'] ?? 0 ) . '%',
                    ];
                }
                $edges[ $ek2 ]['amount'] += (float) $ln['amount'];

                if ( $pid > 0 ) {
                    if ( ! isset( $person_agg[ $pid ] ) ) {
                        $person_agg[ $pid ] = [
                            'person_id' => $pid,
                            'name' => $ln['person_label'],
                            'amount' => 0,
                            'orders' => 0,
                            'by_activity' => [],
                        ];
                    }
                    $person_agg[ $pid ]['amount'] += (float) $ln['amount'];
                    $person_agg[ $pid ]['by_activity'][ $ln['activity_code'] ] =
                        ( $person_agg[ $pid ]['by_activity'][ $ln['activity_code'] ] ?? 0 ) + (float) $ln['amount'];
                }
            }

            foreach ( $alloc['person_shares'] ?? [] as $ps ) {
                $pid = (int) ( $ps['person_id'] ?? 0 );
                if ( $pid && isset( $person_agg[ $pid ] ) ) {
                    $person_agg[ $pid ]['orders']++;
                }
            }
        }

        return [
            'nodes'         => array_values( $nodes ),
            'edges'         => array_values( $edges ),
            'person_shares' => array_values( $person_agg ),
            'orders'        => $orders,
            'role_labels'   => self::role_labels(),
        ];
    }

    public static function person_order_matrix( array $filters = [] ): array {
        $g = self::cost_graph( $filters );
        return [
            'persons' => $g['person_shares'],
            'orders'  => $g['orders'],
        ];
    }
}
