<?php

namespace Modules\MedWorkforce\Services;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\MedWorkforce\Entities\CredentialReview;

/**
 * Credential-compliance dashboard service (SIFA P7-T1 compliance).
 *
 * BINDS to MedOrganization's licence master (org_staff_licenses) — it never
 * duplicates the licence data. It reads that table (Schema::hasTable guard),
 * derives a per-staff compliance status using the SAME expiry window
 * MedOrganization's expiry-alert cron uses, and overlays the periodic review /
 * mandatory-training items this module owns (medwf_credential_reviews).
 *
 * Fail-soft: when MedOrganization is absent the dashboard still renders from the
 * review overlay alone; when ClinicCore/org_user_departments are absent the
 * department column is simply blank.
 */
class ComplianceService
{
    /** Per-licence status keys, worst-first for aggregation/sorting. */
    public const STATUS_EXPIRED  = 'expired';
    public const STATUS_EXPIRING = 'expiring';
    public const STATUS_VALID    = 'valid';
    public const STATUS_MISSING  = 'missing';

    /**
     * Build the whole dashboard payload for a business.
     *
     * @return array{
     *   staff: array<int, array<string,mixed>>,
     *   counts: array<string,int>,
     *   has_org: bool,
     *   expiring_days: int
     * }
     */
    public function buildDashboard(int $businessId): array
    {
        $expiringDays = $this->expiringDays();
        $hasOrg = Schema::hasTable('org_staff_licenses');

        $licensesByUser = $hasOrg ? $this->licensesByUser($businessId) : [];
        $reviewsByUser = $this->reviewsByUser($businessId);
        $departments = $this->departmentByUser($businessId);

        // The staff universe = everyone with a licence OR a review row. (We do not
        // list the entire `users` table — only clinical staff who are actually
        // being credentialed appear on the compliance board.)
        $userIds = array_unique(array_merge(array_keys($licensesByUser), array_keys($reviewsByUser)));

        $names = $this->userNames($userIds);

        $staff = [];
        $counts = [
            self::STATUS_VALID    => 0,
            self::STATUS_EXPIRING => 0,
            self::STATUS_EXPIRED  => 0,
            self::STATUS_MISSING  => 0,
            'reviews_pending'     => 0,
            'reviews_overdue'     => 0,
        ];

        foreach ($userIds as $userId) {
            $licenses = $licensesByUser[$userId] ?? [];
            $reviews = $reviewsByUser[$userId] ?? [];

            $licenseStatus = $this->rollupLicenseStatus($licenses, $expiringDays, $hasOrg);

            $pending = 0;
            $overdue = 0;
            foreach ($reviews as $r) {
                $eff = $this->effectiveReviewStatus($r);
                if ($eff === 'overdue') {
                    $overdue++;
                } elseif ($eff === 'pending') {
                    $pending++;
                }
            }

            $counts[$licenseStatus] = ($counts[$licenseStatus] ?? 0) + 1;
            $counts['reviews_pending'] += $pending;
            $counts['reviews_overdue'] += $overdue;

            $staff[] = [
                'user_id'         => $userId,
                'name'            => $names[$userId] ?? ('#' . $userId),
                'department'      => $departments[$userId] ?? null,
                'license_status'  => $licenseStatus,
                'licenses'        => $licenses,
                'reviews'         => $reviews,
                'reviews_pending' => $pending,
                'reviews_overdue' => $overdue,
            ];
        }

        // Worst status first so problems float to the top of the board.
        $order = [self::STATUS_EXPIRED => 0, self::STATUS_EXPIRING => 1, self::STATUS_MISSING => 2, self::STATUS_VALID => 3];
        usort($staff, function ($a, $b) use ($order) {
            return ($order[$a['license_status']] ?? 9) <=> ($order[$b['license_status']] ?? 9)
                ?: strcmp((string) $a['name'], (string) $b['name']);
        });

        return [
            'staff'         => $staff,
            'counts'        => $counts,
            'has_org'       => $hasOrg,
            'expiring_days' => $expiringDays,
        ];
    }

    /** The expiring window (days). Prefer MedOrganization's own config if published. */
    public function expiringDays(): int
    {
        return (int) config(
            'medorganization.license_alert_days',
            config('medworkforce.credential_expiring_days', 30)
        );
    }

    /**
     * Read org_staff_licenses for a business, grouped by user_id, as plain arrays
     * (no MedOrganization model dependency — pure fail-soft DB read).
     *
     * @return array<int, array<int, array<string,mixed>>>
     */
    protected function licensesByUser(int $businessId): array
    {
        $out = [];

        try {
            $rows = DB::table('org_staff_licenses')
                ->where('business_id', $businessId)
                ->orderBy('expiry_date')
                ->get(['user_id', 'license_type', 'license_number', 'specialization', 'issuing_authority', 'issue_date', 'expiry_date', 'status', 'verified_at']);

            foreach ($rows as $r) {
                $out[(int) $r->user_id][] = [
                    'license_type'      => $r->license_type,
                    'license_number'    => $r->license_number,
                    'specialization'    => $r->specialization,
                    'issuing_authority' => $r->issuing_authority,
                    'issue_date'        => $r->issue_date,
                    'expiry_date'       => $r->expiry_date,
                    'status'            => $r->status,        // pending_verification|verified|rejected|expired
                    'verified'          => ! empty($r->verified_at),
                ];
            }
        } catch (\Throwable $e) {
            \Log::warning('MedWorkforce compliance: could not read org_staff_licenses: ' . $e->getMessage());
        }

        return $out;
    }

    /**
     * The single worst status across a staff member's licences, using the same
     * thresholds as MedOrganization's expiry cron:
     *   - expired:  expiry_date < today OR status already 'expired'
     *   - expiring: today <= expiry_date <= today + window (and not rejected)
     *   - valid:    a verified licence in date
     *   - missing:  MedOrganization present but this user has no licence row
     *
     * @param  array<int, array<string,mixed>>  $licenses
     */
    protected function rollupLicenseStatus(array $licenses, int $expiringDays, bool $hasOrg): string
    {
        if (empty($licenses)) {
            // Only meaningful when MedOrganization is installed; otherwise the
            // caller has already excluded licence-less users, so treat as missing.
            return self::STATUS_MISSING;
        }

        $today = Carbon::today();
        $limit = $today->copy()->addDays($expiringDays);

        $worst = self::STATUS_VALID;
        $rank = [self::STATUS_VALID => 0, self::STATUS_MISSING => 1, self::STATUS_EXPIRING => 2, self::STATUS_EXPIRED => 3];
        $anyValid = false;

        foreach ($licenses as $lic) {
            $status = $lic['status'] ?? null;

            if ($status === 'rejected') {
                // A rejected licence does not count toward validity but is not an
                // expiry problem — skip it.
                continue;
            }

            $expiry = ! empty($lic['expiry_date']) ? Carbon::parse($lic['expiry_date']) : null;

            $thisStatus = self::STATUS_VALID;
            if ($status === 'expired' || ($expiry && $expiry->lt($today))) {
                $thisStatus = self::STATUS_EXPIRED;
            } elseif ($expiry && $expiry->lte($limit)) {
                $thisStatus = self::STATUS_EXPIRING;
            } else {
                $thisStatus = self::STATUS_VALID;
                $anyValid = true;
            }

            if (($rank[$thisStatus] ?? 0) > ($rank[$worst] ?? 0)) {
                $worst = $thisStatus;
            }
        }

        // If every row was rejected/none-valid and nothing was expiring/expired.
        if ($worst === self::STATUS_VALID && ! $anyValid) {
            return self::STATUS_MISSING;
        }

        return $worst;
    }

    /**
     * Read this module's credential-review overlay, grouped by user.
     *
     * @return array<int, array<int, array<string,mixed>>>
     */
    protected function reviewsByUser(int $businessId): array
    {
        $out = [];

        if (! Schema::hasTable('medwf_credential_reviews')) {
            return $out;
        }

        $rows = CredentialReview::forBusiness($businessId)->orderBy('due_date')->get();

        foreach ($rows as $r) {
            $out[(int) $r->user_id][] = [
                'id'             => $r->id,
                'item_type'      => $r->item_type,
                'title'          => $r->title,
                'due_date'       => optional($r->due_date)->toDateString(),
                'completed_date' => optional($r->completed_date)->toDateString(),
                'status'         => $r->status,
                'effective'      => $this->effectiveReviewStatus([
                    'status'   => $r->status,
                    'due_date' => optional($r->due_date)->toDateString(),
                ]),
                'notes'          => $r->notes,
            ];
        }

        return $out;
    }

    /** A still-pending review past its due date reads as overdue. */
    protected function effectiveReviewStatus(array $review): string
    {
        $status = $review['status'] ?? 'pending';
        $due = $review['due_date'] ?? null;

        if ($status === 'pending' && $due && Carbon::parse($due)->isPast()) {
            return 'overdue';
        }

        return $status;
    }

    /**
     * Department name per user via MedOrganization's org_user_departments →
     * clc_departments. Fail-soft: empty map when either table is absent.
     *
     * @param  int[]  $userIds  unused filter hint (kept for signature stability)
     * @return array<int, string>
     */
    protected function departmentByUser(int $businessId): array
    {
        if (! Schema::hasTable('org_user_departments') || ! Schema::hasTable('clc_departments')) {
            return [];
        }

        $map = [];
        try {
            $rows = DB::table('org_user_departments as oud')
                ->join('clc_departments as d', 'd.id', '=', 'oud.department_id')
                ->where('oud.business_id', $businessId)
                ->orderByDesc('oud.is_primary')
                ->get(['oud.user_id', 'd.name']);

            foreach ($rows as $r) {
                // First row per user wins (primary ordered first).
                if (! isset($map[(int) $r->user_id])) {
                    $map[(int) $r->user_id] = $r->name;
                }
            }
        } catch (\Throwable $e) {
            \Log::warning('MedWorkforce compliance: could not read departments: ' . $e->getMessage());
        }

        return $map;
    }

    /**
     * Resolve display names for a set of users from the core `users` table.
     *
     * @param  int[]  $userIds
     * @return array<int, string>
     */
    protected function userNames(array $userIds): array
    {
        if (empty($userIds) || ! Schema::hasTable('users')) {
            return [];
        }

        $names = [];
        $rows = DB::table('users')
            ->whereIn('id', $userIds)
            ->get(['id', 'surname', 'first_name', 'last_name']);

        foreach ($rows as $r) {
            $names[(int) $r->id] = trim(implode(' ', array_filter([$r->surname ?? null, $r->first_name ?? null, $r->last_name ?? null])));
        }

        return $names;
    }
}
