<?php

namespace App\Console\Commands;

use App\Models\Branch;
use App\Models\Customer;
use App\Models\CustomerAppointment;
use App\Models\FinancialAccount;
use App\Models\FixedAsset;
use App\Models\Loan;
use App\Models\LoginAttempt;
use App\Models\ManagementAlert;
use App\Models\ReminderNotificationLog;
use App\Models\Task;
use App\Models\User;
use App\Services\AdvancedFinancialReportService;
use App\Services\BranchContext;
use App\Services\FinancialReportService;
use App\Services\LoanBranchReportService;
use App\Support\Simulation\SimulationExpectations;
use App\Support\Simulation\SimulationOperationsSurfacesExpectations;
use App\Support\Simulation\SimulationReportReconciliationExpectations;
use Illuminate\Console\Command;
use Throwable;

class SimulationValidate extends Command
{
    protected $signature = 'simulation:validate {--json : Output machine-readable JSON}';

    protected $description = 'Validate the fully seeded deterministic microfinance simulation against independent controls';

    public function __construct(
        private BranchContext $branchContext,
        private FinancialReportService $financialReports,
        private AdvancedFinancialReportService $advancedReports,
        private LoanBranchReportService $loanReports,
    ) {
        parent::__construct();
    }

    public function handle(): int
    {
        if (! app()->environment('testing') && ! config('simulation.enabled')) {
            $this->error('Simulation validation is disabled. Set SIMULATION_DATA_ENABLED=true only in an isolated validation environment.');

            return self::FAILURE;
        }

        $checks = [];

        try {
            $this->runChecks($checks);
        } catch (Throwable $exception) {
            $checks[] = [
                'name' => 'validation.execution',
                'status' => 'fail',
                'expected' => 'no exception',
                'actual' => class_basename($exception).': '.$exception->getMessage(),
            ];
        }

        $passed = collect($checks)->every(fn (array $check): bool => $check['status'] === 'pass');
        $report = [
            'schema_version' => 1,
            'status' => $passed ? 'pass' : 'fail',
            'database' => (string) config('database.default'),
            'simulation_phase' => (string) config('simulation.phase'),
            'as_of_date' => (string) config('simulation.as_of_date'),
            'checks' => $checks,
        ];

        if ($this->option('json')) {
            $this->line(json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
        } else {
            foreach ($checks as $check) {
                $this->line(strtoupper($check['status']).': '.$check['name'].' expected='.$check['expected'].' actual='.$check['actual']);
            }

            $passed
                ? $this->info('Full-system simulation validation passed.')
                : $this->error('Full-system simulation validation failed.');
        }

        return $passed ? self::SUCCESS : self::FAILURE;
    }

    /** @param array<int, array{name:string,status:string,expected:string,actual:string}> $checks */
    private function runChecks(array &$checks): void
    {
        $branchCodes = collect(SimulationExpectations::branches())->pluck('code')->values()->all();
        $staffNames = collect(SimulationExpectations::staff())->pluck('username')->values()->all();
        $customerNumbers = collect(SimulationExpectations::customers())->pluck('number')->values()->all();
        $accountNames = collect(SimulationExpectations::accounts())->flatten(1)->pluck('name')->values()->all();

        $branches = $this->branchContext->systemWide(fn () => Branch::query()->whereIn('code', $branchCodes)->get()->keyBy('code'));
        $branchIds = $branches->pluck('id')->map(fn ($id) => (int) $id)->values()->all();

        $this->check($checks, 'foundation.branches', count($branchCodes), $branches->count());
        $this->check($checks, 'foundation.staff', count($staffNames), User::query()->whereIn('username', $staffNames)->count());
        $this->check($checks, 'foundation.customers', count($customerNumbers), $this->branchContext->systemWide(fn () => Customer::query()->whereIn('customer_number', $customerNumbers)->count()));
        $this->check($checks, 'foundation.financial_accounts', count($accountNames), $this->branchContext->systemWide(fn () => FinancialAccount::query()->whereIn('name', $accountNames)->count()));

        $financial = SimulationReportReconciliationExpectations::financialControls();
        $profit = SimulationReportReconciliationExpectations::profitLossControls();
        $portfolio = SimulationReportReconciliationExpectations::portfolioControls();
        $assets = SimulationReportReconciliationExpectations::fixedAssetControls();

        $accountTotal = $this->branchContext->systemWide(fn () => FinancialAccount::query()->whereIn('name', $accountNames)->sum('current_balance'));
        $this->check($checks, 'finance.closing_account_balance', $financial['account_balance'], $this->money($accountTotal));

        $pnl = $this->financialReports->generate(
            'profit-loss',
            $branchIds,
            SimulationReportReconciliationExpectations::PERIOD_FROM,
            SimulationReportReconciliationExpectations::PERIOD_TO,
        );
        $this->check($checks, 'reports.profit_loss.total_income', $profit['total_income'], $pnl['total_income']);
        $this->check($checks, 'reports.profit_loss.total_expenses', $profit['total_expenses'], $pnl['total_expenses']);
        $this->check($checks, 'reports.profit_loss.net_profit', $profit['net_profit'], $pnl['net_profit']);

        $cashFlow = $this->financialReports->generate(
            'cash-flow',
            $branchIds,
            SimulationReportReconciliationExpectations::ALL_TIME_FROM,
            SimulationReportReconciliationExpectations::PERIOD_TO,
        );
        $this->check($checks, 'reports.cash_flow.net_cash_flow', $financial['all_time_net_cash_flow'], $cashFlow['net_cash_flow']);

        $advancedPortfolio = $this->advancedReports->generate(
            'portfolio-summary',
            $branchIds,
            SimulationReportReconciliationExpectations::PERIOD_FROM,
            SimulationReportReconciliationExpectations::PERIOD_TO,
        );
        $this->check($checks, 'portfolio.active_loans', $portfolio['active_loans'], $advancedPortfolio['active_loans']);
        $this->check($checks, 'portfolio.principal_outstanding', $portfolio['principal_outstanding'], $advancedPortfolio['principal_outstanding']);
        $this->check($checks, 'portfolio.total_outstanding', $portfolio['total_outstanding'], $advancedPortfolio['total_outstanding']);
        $this->check($checks, 'portfolio.total_overdue', $portfolio['overdue_total'], $advancedPortfolio['total_overdue']);
        $this->check($checks, 'portfolio.par_percent', $portfolio['portfolio_at_risk_percent'], $advancedPortfolio['portfolio_at_risk_percent']);

        $trialBalance = $this->advancedReports->generate(
            'trial-balance',
            $branchIds,
            SimulationReportReconciliationExpectations::PERIOD_FROM,
            SimulationReportReconciliationExpectations::PERIOD_TO,
        );
        $position = $this->advancedReports->generate(
            'statement-of-financial-position',
            $branchIds,
            SimulationReportReconciliationExpectations::PERIOD_FROM,
            SimulationReportReconciliationExpectations::PERIOD_TO,
        );
        $this->check($checks, 'accounting.trial_balance', true, (bool) $trialBalance['balanced']);
        $this->check($checks, 'accounting.statement_of_financial_position', true, (bool) $position['balanced']);
        $this->check($checks, 'accounting.statement_assets', $profit['statement_assets'], $position['total_assets']);

        $loanPortfolio = $this->branchContext->systemWide(fn () => $this->loanReports->generate(
            'active-loan-portfolio',
            $branchIds,
            SimulationReportReconciliationExpectations::PERIOD_FROM,
            SimulationReportReconciliationExpectations::PERIOD_TO,
        ));
        $this->check($checks, 'loan_report.active_loans', (string) $portfolio['active_loans'], $this->summary($loanPortfolio, 'Active loans'));
        $this->check($checks, 'loan_report.outstanding_principal', $portfolio['principal_outstanding'], $this->summary($loanPortfolio, 'Outstanding principal'));

        $asset = $this->branchContext->systemWide(fn () => FixedAsset::query()->where('name', 'SIM Arusha Office Equipment')->first());
        $this->check($checks, 'fixed_assets.count', $assets['asset_count'], $asset ? 1 : 0);
        $this->check($checks, 'fixed_assets.cost', $assets['total_cost'], $asset?->acquisition_cost ?? '0.00');
        $this->check($checks, 'fixed_assets.accumulated_depreciation', $assets['accumulated_depreciation'], $asset?->accumulated_depreciation ?? '0.00');
        $this->check($checks, 'fixed_assets.net_book_value', $assets['net_book_value'], $asset?->book_value ?? '0.00');

        $overdueLoan = $this->branchContext->systemWide(fn () => Loan::query()->where('purpose', SimulationOperationsSurfacesExpectations::OVERDUE_LOAN_PURPOSE)->first());
        $this->check($checks, 'operations.overdue_reference_loan', 1, $overdueLoan ? 1 : 0);
        $this->check($checks, 'operations.manual_task', 1, $this->branchContext->systemWide(fn () => Task::query()->where('automation_key', SimulationOperationsSurfacesExpectations::ARUSHA_TASK_KEY)->count()));
        $this->check($checks, 'operations.appointments', 2, $this->branchContext->systemWide(fn () => CustomerAppointment::query()->whereIn('title', [
            SimulationOperationsSurfacesExpectations::DAR_APPOINTMENT_TITLE,
            SimulationOperationsSurfacesExpectations::ARUSHA_APPOINTMENT_TITLE,
        ])->count()));

        if ($overdueLoan) {
            $this->check($checks, 'operations.reminder_log', 1, $this->branchContext->systemWide(fn () => ReminderNotificationLog::query()
                ->where('loan_id', $overdueLoan->id)
                ->where('reminder_type', 'overdue')
                ->where('channel', 'in_system')
                ->whereDate('scheduled_for', SimulationOperationsSurfacesExpectations::AS_OF_DATE)
                ->count()));
            $this->check($checks, 'operations.management_alert', 1, $this->branchContext->systemWide(fn () => ManagementAlert::query()
                ->where('alert_key', 'overdue_threshold:'.$overdueLoan->id.':'.SimulationOperationsSurfacesExpectations::OVERDUE_DAYS_THRESHOLD)
                ->count()));
        }

        $loginAttempts = LoginAttempt::query()->whereDate('attempted_at', SimulationOperationsSurfacesExpectations::AS_OF_DATE)
            ->whereIn('login_identifier', ['sim.dar.loan', SimulationOperationsSurfacesExpectations::FAILED_LOGIN_IDENTIFIER])
            ->count();
        $this->check($checks, 'security.login_attempts', 2, $loginAttempts);

        $darManager = User::query()->where('username', 'sim.dar.manager')->first();
        $dar = $branches->get('SIM-DAR');
        $mwanza = $branches->get('SIM-MZA');
        $branchIsolation = $darManager && $dar && $mwanza
            ? $darManager->canAccessBranch((int) $dar->id) && ! $darManager->canAccessBranch((int) $mwanza->id)
            : false;
        $this->check($checks, 'authorization.branch_isolation', true, $branchIsolation);
    }

    /** @param array<int, array{name:string,status:string,expected:string,actual:string}> $checks */
    private function check(array &$checks, string $name, mixed $expected, mixed $actual): void
    {
        $expectedValue = $this->stringify($expected);
        $actualValue = $this->stringify($actual);
        $checks[] = [
            'name' => $name,
            'status' => $expectedValue === $actualValue ? 'pass' : 'fail',
            'expected' => $expectedValue,
            'actual' => $actualValue,
        ];
    }

    private function stringify(mixed $value): string
    {
        if (is_bool($value)) {
            return $value ? 'true' : 'false';
        }

        return (string) $value;
    }

    private function money(mixed $value): string
    {
        return number_format((float) $value, 2, '.', '');
    }

    private function summary(array $report, string $label): string
    {
        return (string) (collect($report['summary'])->firstWhere('label', $label)['value'] ?? '');
    }
}
