<?php

declare(strict_types=1);

namespace App\Services;

use App\Repositories\LegacySpecialtyActivityRepository;
use App\Repositories\SpecialtyFinanceRepository;

final class SpecialtyFinanceActivityParityService
{
    /** @var string */
    private $hospitalCode;

    /** @var SpecialtyFinanceRepository */
    private $current;

    /** @var LegacySpecialtyActivityRepository */
    private $reference;

    public function __construct(string $hospitalCode)
    {
        $this->hospitalCode = strtolower($hospitalCode);
        $this->current = new SpecialtyFinanceRepository($this->hospitalCode);
        $this->reference = new LegacySpecialtyActivityRepository($this->hospitalCode);
    }

    /** @return array<string, mixed> */
    public function run(
        int $year,
        int $compareYear,
        int $startMonth,
        int $endMonth
    ): array {
        $currentContract = $this->current->activitySourceContract();
        $referenceContract = $this->reference->sourceContract();

        if (empty($currentContract['ready']) || empty($referenceContract['ready'])) {
            return [
                'schema' => 'DDB_SPECIALTY_FINANCE_ACTIVITY_PARITY_V1_1',
                'hospital' => strtoupper($this->hospitalCode),
                'year' => $year,
                'compare_year' => $compareYear,
                'start_month' => $startMonth,
                'end_month' => $endMonth,
                'source_contract' => [
                    'current' => $currentContract,
                    'reference' => $referenceContract,
                ],
                'checks' => [],
                'failed' => ['source_contract_ready'],
                'status' => 'FAIL',
                'oracle_writes' => 'NONE',
            ];
        }

        $comparison = $this->current->specialtyActivityComparison(
            $year,
            $compareYear,
            $startMonth,
            $endMonth
        );
        $monthlyComparison = $this->current->specialtyActivityMonthlyComparison(
            $year,
            $compareYear,
            $startMonth,
            $endMonth
        );

        $referenceCurrent = $this->reference->specialtyActivityForYear(
            $year,
            $startMonth,
            $endMonth
        );
        $referenceCompare = $this->reference->specialtyActivityForYear(
            $compareYear,
            $startMonth,
            $endMonth
        );
        $referenceMonthlyCurrent = $this->reference->specialtyActivityMonthlyForYear(
            $year,
            $startMonth,
            $endMonth
        );
        $referenceMonthlyCompare = $this->reference->specialtyActivityMonthlyForYear(
            $compareYear,
            $startMonth,
            $endMonth
        );

        $checks = [];
        $checks['source_contract_ready'] = !empty($currentContract['ready'])
            && !empty($referenceContract['ready']);
        $checks['specialty_period_rows_present'] = count($comparison) > 0;
        $checks['specialty_month_rows_present'] = count($monthlyComparison) > 0;

        $zeroOnlyRows = [
            'period' => [
                'current' => $this->zeroOnlyPeriodIdentities($comparison, 'current'),
                'compare' => $this->zeroOnlyPeriodIdentities($comparison, 'compare'),
            ],
            'monthly' => [
                'current' => $this->zeroOnlyMonthlyIdentities($monthlyComparison, 'current'),
                'compare' => $this->zeroOnlyMonthlyIdentities($monthlyComparison, 'compare'),
            ],
        ];

        $periodCurrent = $this->periodMap($comparison, 'current');
        $periodCompare = $this->periodMap($comparison, 'compare');
        $referencePeriodCurrent = $this->referencePeriodMap($referenceCurrent);
        $referencePeriodCompare = $this->referencePeriodMap($referenceCompare);

        $periodCurrentDiff = $this->compareMaps($periodCurrent, $referencePeriodCurrent);
        $periodCompareDiff = $this->compareMaps($periodCompare, $referencePeriodCompare);

        $checks['current_period_exact'] = $periodCurrentDiff['max_difference'] === 0.0
            && $periodCurrentDiff['identity_mismatches'] === [];
        $checks['compare_period_exact'] = $periodCompareDiff['max_difference'] === 0.0
            && $periodCompareDiff['identity_mismatches'] === [];

        $monthlyCurrent = $this->monthlyMap($monthlyComparison, 'current');
        $monthlyCompare = $this->monthlyMap($monthlyComparison, 'compare');
        $referenceMonthlyCurrentMap = $this->referenceMonthlyMap($referenceMonthlyCurrent);
        $referenceMonthlyCompareMap = $this->referenceMonthlyMap($referenceMonthlyCompare);

        $monthlyCurrentDiff = $this->compareMaps(
            $monthlyCurrent,
            $referenceMonthlyCurrentMap
        );
        $monthlyCompareDiff = $this->compareMaps(
            $monthlyCompare,
            $referenceMonthlyCompareMap
        );

        $checks['current_monthly_exact'] = $monthlyCurrentDiff['max_difference'] === 0.0
            && $monthlyCurrentDiff['identity_mismatches'] === [];
        $checks['compare_monthly_exact'] = $monthlyCompareDiff['max_difference'] === 0.0
            && $monthlyCompareDiff['identity_mismatches'] === [];

        $currentDoctorTotals = $this->current->activityDoctorTotals(
            $year,
            $startMonth,
            $endMonth
        );
        $compareDoctorTotals = $this->current->activityDoctorTotals(
            $compareYear,
            $startMonth,
            $endMonth
        );

        $currentSpecialtyTotals = $this->sumPeriodMap($periodCurrent);
        $compareSpecialtyTotals = $this->sumPeriodMap($periodCompare);

        $currentReconciliation = [
            'outpatient_visits' => abs(
                $currentSpecialtyTotals['outpatient_visits']
                - $currentDoctorTotals['outpatient_visits']
            ),
            'admissions' => abs(
                $currentSpecialtyTotals['admissions']
                - $currentDoctorTotals['admissions']
            ),
        ];
        $compareReconciliation = [
            'outpatient_visits' => abs(
                $compareSpecialtyTotals['outpatient_visits']
                - $compareDoctorTotals['outpatient_visits']
            ),
            'admissions' => abs(
                $compareSpecialtyTotals['admissions']
                - $compareDoctorTotals['admissions']
            ),
        ];

        $checks['current_specialty_total_reconciles_to_doctor_total'] =
            $currentReconciliation['outpatient_visits'] === 0.0
            && $currentReconciliation['admissions'] === 0.0;
        $checks['compare_specialty_total_reconciles_to_doctor_total'] =
            $compareReconciliation['outpatient_visits'] === 0.0
            && $compareReconciliation['admissions'] === 0.0;

        $checks['both_metrics_have_activity'] =
            $currentSpecialtyTotals['outpatient_visits'] > 0
            && $currentSpecialtyTotals['admissions'] > 0
            && $compareSpecialtyTotals['outpatient_visits'] > 0
            && $compareSpecialtyTotals['admissions'] > 0;

        $checks['monthly_grain_within_contract'] = $this->monthlyRowsInRange(
            $monthlyComparison,
            $startMonth,
            $endMonth
        );
        $checks['zero_only_side_rows_normalized'] =
            $this->mapsContainNoZeroOnlyRows($periodCurrent)
            && $this->mapsContainNoZeroOnlyRows($periodCompare)
            && $this->mapsContainNoZeroOnlyRows($monthlyCurrent)
            && $this->mapsContainNoZeroOnlyRows($monthlyCompare);

        $failed = [];
        foreach ($checks as $name => $passed) {
            if (!$passed) {
                $failed[] = $name;
            }
        }

        return [
            'schema' => 'DDB_SPECIALTY_FINANCE_ACTIVITY_PARITY_V1_1',
            'captured_at' => date(DATE_ATOM),
            'hospital' => strtoupper($this->hospitalCode),
            'year' => $year,
            'compare_year' => $compareYear,
            'start_month' => $startMonth,
            'end_month' => $endMonth,
            'source_contract' => [
                'current' => $currentContract,
                'reference' => $referenceContract,
            ],
            'counts' => [
                'combined_period_specialties' => count($comparison),
                'combined_monthly_rows' => count($monthlyComparison),
                'normalized_current_period_specialties' => count($periodCurrent),
                'normalized_compare_period_specialties' => count($periodCompare),
                'normalized_current_monthly_rows' => count($monthlyCurrent),
                'normalized_compare_monthly_rows' => count($monthlyCompare),
                'reference_current_specialties' => count($referenceCurrent),
                'reference_compare_specialties' => count($referenceCompare),
            ],
            'normalization' => [
                'rule' => 'A row with zero Admissions and zero Outpatient Visits is excluded only from that year-side parity identity set. The combined comparison row remains available for new or discontinued specialty detection.',
                'zero_only_rows' => $zeroOnlyRows,
            ],
            'period_parity' => [
                'current' => $periodCurrentDiff,
                'compare' => $periodCompareDiff,
            ],
            'monthly_parity' => [
                'current' => $monthlyCurrentDiff,
                'compare' => $monthlyCompareDiff,
            ],
            'reconciliation' => [
                'current' => [
                    'specialty_totals' => $currentSpecialtyTotals,
                    'doctor_totals' => $currentDoctorTotals,
                    'difference' => $currentReconciliation,
                ],
                'compare' => [
                    'specialty_totals' => $compareSpecialtyTotals,
                    'doctor_totals' => $compareDoctorTotals,
                    'difference' => $compareReconciliation,
                ],
            ],
            'checks' => $checks,
            'failed' => $failed,
            'status' => $failed === [] ? 'PASS' : 'FAIL',
            'oracle_writes' => 'NONE',
        ];
    }

    /** @return array<string, array<string, mixed>> */
    private function periodMap(array $rows, string $prefix): array
    {
        $map = [];
        foreach ($rows as $row) {
            $id = trim((string)($row['id'] ?? ''));
            if ($id === '') {
                continue;
            }

            $outpatientVisits = (float)(
                $row[$prefix . '_outpatient_visits'] ?? 0
            );
            $admissions = (float)($row[$prefix . '_admissions'] ?? 0);

            if ($this->isZeroActivity($outpatientVisits, $admissions)) {
                continue;
            }

            $map[$id] = [
                'label' => trim((string)($row['label'] ?? '')),
                'outpatient_visits' => $outpatientVisits,
                'admissions' => $admissions,
            ];
        }
        ksort($map);
        return $map;
    }

    /**
     * @return array<int, string>
     */
    private function zeroOnlyPeriodIdentities(array $rows, string $prefix): array
    {
        $identities = [];
        foreach ($rows as $row) {
            $id = trim((string)($row['id'] ?? ''));
            if ($id === '') {
                continue;
            }

            $outpatientVisits = (float)(
                $row[$prefix . '_outpatient_visits'] ?? 0
            );
            $admissions = (float)($row[$prefix . '_admissions'] ?? 0);

            if ($this->isZeroActivity($outpatientVisits, $admissions)) {
                $identities[] = $id;
            }
        }

        sort($identities);
        return array_values(array_unique($identities));
    }

    /**
     * @return array<int, string>
     */
    private function zeroOnlyMonthlyIdentities(array $rows, string $prefix): array
    {
        $identities = [];
        foreach ($rows as $row) {
            $id = trim((string)($row['id'] ?? ''));
            $month = (int)($row['month_id'] ?? 0);
            if ($id === '' || $month < 1 || $month > 12) {
                continue;
            }

            $outpatientVisits = (float)(
                $row[$prefix . '_outpatient_visits'] ?? 0
            );
            $admissions = (float)($row[$prefix . '_admissions'] ?? 0);

            if ($this->isZeroActivity($outpatientVisits, $admissions)) {
                $identities[] = $id . '|' . $month;
            }
        }

        sort($identities);
        return array_values(array_unique($identities));
    }

    private function isZeroActivity(
        float $outpatientVisits,
        float $admissions
    ): bool {
        return $outpatientVisits === 0.0 && $admissions === 0.0;
    }

    private function mapsContainNoZeroOnlyRows(array $map): bool
    {
        foreach ($map as $row) {
            if ($this->isZeroActivity(
                (float)($row['outpatient_visits'] ?? 0),
                (float)($row['admissions'] ?? 0)
            )) {
                return false;
            }
        }

        return true;
    }

    /** @return array<string, array<string, mixed>> */
    private function referencePeriodMap(array $rows): array
    {
        $map = [];
        foreach ($rows as $row) {
            $id = trim((string)($row['id'] ?? ''));
            if ($id === '') {
                continue;
            }
            $map[$id] = [
                'label' => trim((string)($row['label'] ?? '')),
                'outpatient_visits' => (float)($row['outpatient_visits'] ?? 0),
                'admissions' => (float)($row['admissions'] ?? 0),
            ];
        }
        ksort($map);
        return $map;
    }

    /** @return array<string, array<string, mixed>> */
    private function monthlyMap(array $rows, string $prefix): array
    {
        $map = [];
        foreach ($rows as $row) {
            $id = trim((string)($row['id'] ?? ''));
            $month = (int)($row['month_id'] ?? 0);
            if ($id === '' || $month < 1 || $month > 12) {
                continue;
            }

            $outpatientVisits = (float)(
                $row[$prefix . '_outpatient_visits'] ?? 0
            );
            $admissions = (float)($row[$prefix . '_admissions'] ?? 0);

            if ($this->isZeroActivity($outpatientVisits, $admissions)) {
                continue;
            }

            $key = $id . '|' . $month;
            $map[$key] = [
                'label' => trim((string)($row['label'] ?? '')),
                'outpatient_visits' => $outpatientVisits,
                'admissions' => $admissions,
            ];
        }
        ksort($map);
        return $map;
    }

    /** @return array<string, array<string, mixed>> */
    private function referenceMonthlyMap(array $rows): array
    {
        $map = [];
        foreach ($rows as $row) {
            $id = trim((string)($row['id'] ?? ''));
            $month = (int)($row['month_id'] ?? 0);
            if ($id === '' || $month < 1 || $month > 12) {
                continue;
            }
            $key = $id . '|' . $month;
            $map[$key] = [
                'label' => trim((string)($row['label'] ?? '')),
                'outpatient_visits' => (float)($row['outpatient_visits'] ?? 0),
                'admissions' => (float)($row['admissions'] ?? 0),
            ];
        }
        ksort($map);
        return $map;
    }

    /** @return array<string, mixed> */
    private function compareMaps(array $actual, array $reference): array
    {
        $keys = array_values(array_unique(array_merge(
            array_keys($actual),
            array_keys($reference)
        )));
        sort($keys);

        $identityMismatches = [];
        $valueDifferences = [];
        $maxDifference = 0.0;

        foreach ($keys as $key) {
            if (!isset($actual[$key]) || !isset($reference[$key])) {
                $identityMismatches[] = $key;
                continue;
            }

            foreach (['outpatient_visits', 'admissions'] as $metric) {
                $difference = abs(
                    (float)$actual[$key][$metric]
                    - (float)$reference[$key][$metric]
                );
                if ($difference > 0.0) {
                    $valueDifferences[] = [
                        'key' => $key,
                        'metric' => $metric,
                        'actual' => (float)$actual[$key][$metric],
                        'reference' => (float)$reference[$key][$metric],
                        'difference' => $difference,
                    ];
                }
                $maxDifference = max($maxDifference, $difference);
            }
        }

        return [
            'actual_rows' => count($actual),
            'reference_rows' => count($reference),
            'identity_mismatches' => $identityMismatches,
            'value_differences' => $valueDifferences,
            'max_difference' => $maxDifference,
            'status' => $identityMismatches === []
                && $maxDifference === 0.0
                ? 'PASS'
                : 'FAIL',
        ];
    }

    /** @return array<string, float> */
    private function sumPeriodMap(array $map): array
    {
        $totals = [
            'outpatient_visits' => 0.0,
            'admissions' => 0.0,
        ];
        foreach ($map as $row) {
            $totals['outpatient_visits'] += (float)$row['outpatient_visits'];
            $totals['admissions'] += (float)$row['admissions'];
        }
        return $totals;
    }

    private function monthlyRowsInRange(
        array $rows,
        int $startMonth,
        int $endMonth
    ): bool {
        if ($rows === []) {
            return false;
        }
        foreach ($rows as $row) {
            $month = (int)($row['month_id'] ?? 0);
            if ($month < $startMonth || $month > $endMonth) {
                return false;
            }
        }
        return true;
    }
}
