<?php

declare(strict_types=1);

namespace App\Repositories;

use RuntimeException;

final class InpatientDailyRevenueRuleSettingsRepository
{
    private string $hospitalCode;

    public function __construct(string $hospitalCode)
    {
        $this->hospitalCode = \normalize_hospital_code($hospitalCode);
    }

    /** @return array<string,mixed> */
    public function load(): array
    {
        $defaults = $this->defaults();
        $file = $this->settingsFile();
        if (!is_file($file)) {
            return $defaults;
        }

        $raw = @file_get_contents($file);
        if (!is_string($raw) || trim($raw) === '') {
            return $defaults;
        }

        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            throw new RuntimeException('Daily revenue settings file is not valid JSON.');
        }

        return $this->normalize($decoded, $defaults);
    }

    /** @return array<string,mixed> */
    public function defaults(): array
    {
        $rules = \config('inpatient_daily_revenue_rules.branches.' . $this->hospitalCode, []);
        if (!is_array($rules) || $rules === []) {
            throw new RuntimeException('Daily revenue business rules are not configured for this branch.');
        }

        return $this->normalize($rules, $rules);
    }

    /**
     * @param array<string,mixed> $input
     * @return array<string,mixed>
     */
    public function save(array $input, string $changedBy): array
    {
        $before = $this->load();
        $after = $this->normalize($input, $before);
        $this->validateCrossRuleIntegrity($after);

        $dir = dirname($this->settingsFile());
        if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) {
            throw new RuntimeException('Unable to create the Daily Revenue settings directory.');
        }

        $this->backupCurrentFile();

        $payload = json_encode($after, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($payload)) {
            throw new RuntimeException('Unable to encode Daily Revenue settings.');
        }
        $payload .= PHP_EOL;

        $target = $this->settingsFile();
        $token = bin2hex(random_bytes(6));
        $tmp = $target . '.tmp.' . $token;
        $swap = $target . '.rollback.' . $token;
        $hadCurrent = is_file($target);

        $written = @file_put_contents($tmp, $payload, LOCK_EX);
        if ($written === false || $written !== strlen($payload)) {
            @unlink($tmp);
            throw new RuntimeException('Unable to write Daily Revenue settings atomically.');
        }

        if ($hadCurrent && !@rename($target, $swap)) {
            @unlink($tmp);
            throw new RuntimeException('Unable to stage the current Daily Revenue settings for rollback.');
        }

        if (!@rename($tmp, $target)) {
            @unlink($tmp);
            if ($hadCurrent && is_file($swap)) {
                @rename($swap, $target);
            }
            throw new RuntimeException('Unable to activate Daily Revenue settings.');
        }

        try {
            $this->appendAudit($before, $after, $changedBy);
        } catch (\Throwable $e) {
            @unlink($target);
            if ($hadCurrent && is_file($swap) && !@rename($swap, $target)) {
                throw new RuntimeException('Daily Revenue settings audit failed and automatic rollback could not restore the previous settings.', 0, $e);
            }
            throw $e;
        }

        if (is_file($swap)) {
            @unlink($swap);
        }
        return $after;
    }

    /** @return array<string,mixed> */
    public function restoreDefaults(string $changedBy): array
    {
        return $this->save($this->defaults(), $changedBy);
    }

    public function fingerprint(array $rules): string
    {
        return hash('sha256', (string)json_encode($rules, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    /**
     * @param array<string,mixed> $input
     * @param array<string,mixed> $fallback
     * @return array<string,mixed>
     */
    private function normalize(array $input, array $fallback): array
    {
        $nationalInput = is_array($input['national_payer'] ?? null) ? $input['national_payer'] : [];
        $nationalFallback = is_array($fallback['national_payer'] ?? null) ? $fallback['national_payer'] : [];
        $tierInput = is_array($input['tier_schedule'] ?? null) ? $input['tier_schedule'] : [];
        $tierFallback = is_array($fallback['tier_schedule'] ?? null) ? $fallback['tier_schedule'] : [];
        $payersInput = is_array($input['tiered_payers'] ?? null) ? $input['tiered_payers'] : [];
        $payersFallback = is_array($fallback['tiered_payers'] ?? null) ? $fallback['tiered_payers'] : [];
        $cashInput = is_array($input['cash'] ?? null) ? $input['cash'] : [];
        $cashFallback = is_array($fallback['cash'] ?? null) ? $fallback['cash'] : [];
        $otherFallback = is_array($fallback['other_insurance'] ?? null) ? $fallback['other_insurance'] : [];

        $icuTypesRaw = $nationalInput['icu_types'] ?? ($nationalFallback['icu_types'] ?? []);
        if (is_string($icuTypesRaw)) {
            $icuTypesRaw = preg_split('/[\r\n,]+/', $icuTypesRaw) ?: [];
        }
        $icuTypes = [];
        foreach ((array)$icuTypesRaw as $type) {
            $value = trim((string)$type);
            if ($value !== '' && !in_array(strtolower($value), array_map('strtolower', $icuTypes), true)) {
                $icuTypes[] = $value;
            }
        }

        $nicuTypesRaw = $nationalInput['nicu_types'] ?? ($nationalFallback['nicu_types'] ?? ['NICU']);
        if (is_string($nicuTypesRaw)) {
            $nicuTypesRaw = preg_split('/[\r\n,]+/', $nicuTypesRaw) ?: [];
        }
        $nicuTypes = [];
        foreach ((array)$nicuTypesRaw as $type) {
            $value = trim((string)$type);
            if ($value !== '' && !in_array(strtolower($value), array_map('strtolower', $nicuTypes), true)) {
                $nicuTypes[] = $value;
            }
        }

        $cashIdsRaw = $cashInput['company_ids'] ?? ($cashFallback['company_ids'] ?? [1]);
        if (is_string($cashIdsRaw)) {
            $cashIdsRaw = preg_split('/[\s,;]+/', $cashIdsRaw) ?: [];
        }
        $cashIds = [];
        foreach ((array)$cashIdsRaw as $id) {
            $companyId = (int)$id;
            if ($companyId > 0 && !in_array($companyId, $cashIds, true)) {
                $cashIds[] = $companyId;
            }
        }
        if ($cashIds === []) {
            $cashIds = [1];
        }

        $normalized = [
            'national_payer' => [
                'company_id' => $this->positiveInt($nationalInput['company_id'] ?? ($nationalFallback['company_id'] ?? 0), 'National payer company code'),
                'company_name' => $this->text($nationalInput['company_name'] ?? ($nationalFallback['company_name'] ?? ''), 'National payer company name'),
                'short_stay_icu_rate' => $this->nonNegativeFloat($nationalInput['short_stay_icu_rate'] ?? ($nationalFallback['short_stay_icu_rate'] ?? 0), 'Short-stay ICU rate'),
                'standard_rate' => $this->nonNegativeFloat($nationalInput['standard_rate'] ?? ($nationalFallback['standard_rate'] ?? 0), 'Standard national payer rate'),
                'short_stay_days_less_than' => $this->positiveInt($nationalInput['short_stay_days_less_than'] ?? ($nationalFallback['short_stay_days_less_than'] ?? 30), 'Short-stay threshold'),
                'icu_types' => $icuTypes,
                'nicu_types' => $nicuTypes,
                'nicu_days_up_to' => $this->positiveInt($nationalInput['nicu_days_up_to'] ?? ($nationalFallback['nicu_days_up_to'] ?? 14), 'NICU day threshold'),
                'nicu_rate_days_1_to_limit' => $this->nonNegativeFloat($nationalInput['nicu_rate_days_1_to_limit'] ?? ($nationalFallback['nicu_rate_days_1_to_limit'] ?? 5232.15), 'NICU days 1-to-limit rate'),
                'nicu_rate_after_limit' => $this->nonNegativeFloat($nationalInput['nicu_rate_after_limit'] ?? ($nationalFallback['nicu_rate_after_limit'] ?? 3251.77), 'NICU after-limit rate'),
                'los_source' => 'continuous_inpatient_chain_from_rcp_visit_classcode_1',
            ],
            'tier_schedule' => [
                'days_1_3' => $this->nonNegativeFloat($tierInput['days_1_3'] ?? ($tierFallback['days_1_3'] ?? 0), 'Days 1-3 rate'),
                'days_4_8' => $this->nonNegativeFloat($tierInput['days_4_8'] ?? ($tierFallback['days_4_8'] ?? 0), 'Days 4-8 rate'),
                'days_9_plus' => $this->nonNegativeFloat($tierInput['days_9_plus'] ?? ($tierFallback['days_9_plus'] ?? 0), 'Days 9+ rate'),
                'los_source' => 'continuous_inpatient_chain_from_rcp_visit_classcode_1',
            ],
            'tiered_payers' => [
                'tcs' => $this->normalizeTieredPayer($payersInput, $payersFallback, 'tcs', 'TCS'),
                'gosi' => $this->normalizeTieredPayer($payersInput, $payersFallback, 'gosi', 'GOSI'),
            ],
            'cash' => [
                'company_ids' => $cashIds,
                'formula' => 'patient_share_per_los',
                'los_source' => 'continuous_inpatient_chain_from_rcp_visit_classcode_1',
            ],
            'other_insurance' => [
                'formula' => (string)($otherFallback['formula'] ?? 'company_share_per_los'),
                'los_source' => 'continuous_inpatient_chain_from_rcp_visit_classcode_1',
            ],
        ];

        $this->validateCrossRuleIntegrity($normalized);
        return $normalized;
    }

    /**
     * @param array<string,mixed> $input
     * @param array<string,mixed> $fallback
     * @return array{company_id:int,company_name:string}
     */
    private function normalizeTieredPayer(array $input, array $fallback, string $key, string $label): array
    {
        $row = is_array($input[$key] ?? null) ? $input[$key] : [];
        $base = is_array($fallback[$key] ?? null) ? $fallback[$key] : [];
        return [
            'company_id' => $this->positiveInt($row['company_id'] ?? ($base['company_id'] ?? 0), $label . ' company code'),
            'company_name' => $this->text($row['company_name'] ?? ($base['company_name'] ?? $label), $label . ' company name'),
        ];
    }

    /** @param array<string,mixed> $rules */
    private function validateCrossRuleIntegrity(array $rules): void
    {
        $nationalId = (int)$rules['national_payer']['company_id'];
        $tcsId = (int)$rules['tiered_payers']['tcs']['company_id'];
        $gosiId = (int)$rules['tiered_payers']['gosi']['company_id'];
        $cashIds = array_map('intval', (array)$rules['cash']['company_ids']);
        $fixedIds = [$nationalId, $tcsId, $gosiId];

        if (count(array_unique($fixedIds)) !== count($fixedIds)) {
            throw new RuntimeException('National payer, TCS and GOSI company codes must be different.');
        }
        foreach ($fixedIds as $id) {
            if (in_array($id, $cashIds, true)) {
                throw new RuntimeException('A fixed-rate payer company code cannot also be configured as CASH.');
            }
        }
        if ((array)$rules['national_payer']['icu_types'] === []) {
            throw new RuntimeException('At least one ICU nursing-unit type is required.');
        }
        if ((array)$rules['national_payer']['nicu_types'] === []) {
            throw new RuntimeException('At least one NICU nursing-unit type is required.');
        }
    }

    private function positiveInt($value, string $label): int
    {
        $number = filter_var($value, FILTER_VALIDATE_INT);
        if ($number === false || (int)$number <= 0) {
            throw new RuntimeException($label . ' must be a positive integer.');
        }
        return (int)$number;
    }

    private function nonNegativeFloat($value, string $label): float
    {
        if (!is_numeric($value) || !is_finite((float)$value) || (float)$value < 0) {
            throw new RuntimeException($label . ' must be a non-negative number.');
        }
        return round((float)$value, 2);
    }

    private function text($value, string $label): string
    {
        $text = trim((string)$value);
        if ($text === '') {
            throw new RuntimeException($label . ' is required.');
        }
        return substr($text, 0, 160);
    }

    private function settingsFile(): string
    {
        return \storage_path('inpatient_daily_revenue_rules/' . $this->hospitalCode . '.json');
    }

    private function backupCurrentFile(): void
    {
        $file = $this->settingsFile();
        if (!is_file($file)) {
            return;
        }

        $dir = \storage_path('inpatient_daily_revenue_rules/backups/' . $this->hospitalCode);
        if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) {
            throw new RuntimeException('Unable to create the Daily Revenue settings backup directory.');
        }
        $target = $dir . DIRECTORY_SEPARATOR . date('Ymd_His') . '_' . bin2hex(random_bytes(3)) . '.json';
        if (!@copy($file, $target)) {
            throw new RuntimeException('Unable to create a backup before saving Daily Revenue settings.');
        }
    }

    /**
     * @param array<string,mixed> $before
     * @param array<string,mixed> $after
     */
    private function appendAudit(array $before, array $after, string $changedBy): void
    {
        $dir = \storage_path('inpatient_daily_revenue_rules/audit');
        if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) {
            throw new RuntimeException('Unable to create the Daily Revenue settings audit directory.');
        }

        $record = [
            'timestamp' => date('c'),
            'hospital' => $this->hospitalCode,
            'changed_by' => trim($changedBy) !== '' ? trim($changedBy) : 'unknown',
            'before_sha256' => $this->fingerprint($before),
            'after_sha256' => $this->fingerprint($after),
            'oracle_writes' => 'NONE',
        ];
        $line = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($line) || @file_put_contents($dir . DIRECTORY_SEPARATOR . $this->hospitalCode . '.jsonl', $line . PHP_EOL, FILE_APPEND | LOCK_EX) === false) {
            throw new RuntimeException('Unable to append the Daily Revenue settings audit trail.');
        }
    }
}
