<?php

declare(strict_types=1);

namespace Kylin\Medical\Services;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Kylin\Medical\Enums\AppointmentStatus;
use Kylin\Medical\Enums\MedicalRecordStatus;
use Kylin\Medical\Enums\PrescriptionStatus;
use Kylin\Medical\Enums\PrescriptionType;
use Kylin\Medical\Models\Appointment;
use Kylin\Medical\Models\Consultation;
use Kylin\Medical\Models\MedicalRecord;
use Kylin\Medical\Models\Patient;
use Kylin\Medical\Models\Prescription;
use Kylin\Medical\Models\PrescriptionItem;
use Kylin\Medical\Models\Schedule;
use Throwable;

/**
 * 医疗服务
 *
 * 提供预约挂号、退号、处方开具、药师审核、电子病历签名等核心业务能力。
 *
 * 法律合规：
 *  - 《处方管理办法》第七条：处方用量限制（普通 7 天 / 急诊 3 天）
 *  - 《处方管理办法》第十八条：每张处方不得超过 5 种药品
 *  - 《电子病历应用管理规范》第十条：电子病历应使用电子签名
 *  - 《电子病历应用管理规范》第二十二条：门诊病历保存 >= 15 年，住院病历保存 >= 30 年
 *  - 《个人信息保护法》第二十八条：医疗健康信息属于敏感个人信息
 *  - 《网络安全法》第二十一条：操作日志留存 >= 180 天
 *
 * 所有关键操作（挂号/退号/开方/审方/签名）均写入审计日志，便于追溯。
 */
class MedicalService
{
    /**
     * 创建预约挂号
     *
     * @param array{
     *     patient_id: int,
     *     doctor_id: int,
     *     schedule_id: int,
     * } $data
     * @return Appointment
     *
     * @throws \RuntimeException 当号源不足或排班不存在
     */
    public function createAppointment(array $data): Appointment
    {
        return DB::transaction(function () use ($data) {
            /** @var Schedule $schedule */
            $schedule = Schedule::lockForUpdate()->findOrFail($data['schedule_id']);

            if (!$schedule->status) {
                throw new \RuntimeException('该排班已停用');
            }

            if ($schedule->remaining <= 0) {
                throw new \RuntimeException('号源已约满');
            }

            // 扣减号源
            $schedule->decrement('remaining');

            /** @var Appointment $appointment */
            $appointment = Appointment::create([
                'patient_id' => $data['patient_id'],
                'doctor_id' => $data['doctor_id'],
                'schedule_id' => $schedule->id,
                'appointment_no' => $this->generateAppointmentNo(),
                'status' => AppointmentStatus::PENDING,
                'fee' => $schedule->fee,
            ]);

            $this->audit('appointment_created', Appointment::class, $appointment->id, [
                'appointment_no' => $appointment->appointment_no,
                'patient_id' => $appointment->patient_id,
                'doctor_id' => $appointment->doctor_id,
                'schedule_id' => $schedule->id,
                'fee' => $appointment->fee,
            ]);

            return $appointment;
        });
    }

    /**
     * 退号
     *
     * 依据《医疗机构门诊挂号管理规范》，退号需提前一定时间（默认 24 小时）。
     *
     * @throws \RuntimeException 当超过退号时限或状态不允许
     */
    public function cancelAppointment(int $appointmentId, string $reason = ''): Appointment
    {
        return DB::transaction(function () use ($appointmentId, $reason) {
            /** @var Appointment $appointment */
            $appointment = Appointment::lockForUpdate()->findOrFail($appointmentId);

            if ($appointment->status !== AppointmentStatus::PENDING) {
                throw new \RuntimeException('当前状态不允许退号');
            }

            // 校验退号时限
            $cancelBeforeHours = (int) config('kylin-medical.appointment.cancel_before_hours', 24);
            $schedule = $appointment->schedule;
            $appointmentTime = strtotime($schedule->date->format('Y-m-d') . ' ' . $schedule->time_slot);

            if ($appointmentTime !== false && (time() + $cancelBeforeHours * 3600) > $appointmentTime) {
                throw new \RuntimeException("退号需提前 {$cancelBeforeHours} 小时");
            }

            // 释放号源
            $schedule->increment('remaining');

            $appointment->update([
                'status' => AppointmentStatus::CANCELLED,
                'cancelled_at' => now(),
                'cancel_reason' => $reason,
            ]);

            $this->audit('appointment_cancelled', Appointment::class, $appointment->id, [
                'appointment_no' => $appointment->appointment_no,
                'reason' => $reason,
            ]);

            return $appointment;
        });
    }

    /**
     * 开具处方
     *
     * 依据《处方管理办法》：
     *  - 第七条：普通处方 <= 7 天，急诊处方 <= 3 天
     *  - 第十八条：每张处方 <= 5 种药品
     *
     * @param array{
     *     medical_record_id: int,
     *     doctor_id: int,
     *     type: string,
     *     items: array<int, array{drug_id: int, dosage: string, frequency: string, days: int, usage_method?: string, remark?: string}>
     * } $data
     * @return Prescription
     *
     * @throws \RuntimeException 当违反处方规则
     */
    public function createPrescription(array $data): Prescription
    {
        $maxItems = (int) config('kylin-medical.prescription.max_items_per_prescription', 5);
        $normalDays = (int) config('kylin-medical.prescription.normal_valid_days', 7);
        $emergencyDays = (int) config('kylin-medical.prescription.emergency_valid_days', 3);

        $items = $data['items'] ?? [];
        if (count($items) === 0) {
            throw new \RuntimeException('处方至少需要一种药品');
        }

        // 校验药品数量上限
        if (count($items) > $maxItems) {
            throw new \RuntimeException("每张处方不得超过 {$maxItems} 种药品（《处方管理办法》第十八条）");
        }

        // 校验处方用量
        $type = $data['type'] ?? PrescriptionType::NORMAL->value;
        $maxDays = $type === PrescriptionType::EMERGENCY->value ? $emergencyDays : $normalDays;

        foreach ($items as $item) {
            if (($item['days'] ?? 0) > $maxDays) {
                $label = $type === PrescriptionType::EMERGENCY->value ? '急诊' : '普通';
                throw new \RuntimeException("{$label}处方不得超过 {$maxDays} 天用量（《处方管理办法》第七条）");
            }
        }

        return DB::transaction(function () use ($data, $items, $type, $maxDays) {
            /** @var Prescription $prescription */
            $prescription = Prescription::create([
                'medical_record_id' => $data['medical_record_id'],
                'doctor_id' => $data['doctor_id'],
                'status' => PrescriptionStatus::PENDING,
                'type' => $type,
                'valid_until' => now()->addDays($maxDays)->toDateString(),
            ]);

            foreach ($items as $item) {
                PrescriptionItem::create([
                    'prescription_id' => $prescription->id,
                    'drug_id' => $item['drug_id'],
                    'dosage' => $item['dosage'],
                    'frequency' => $item['frequency'],
                    'days' => $item['days'],
                    'usage_method' => $item['usage_method'] ?? null,
                    'remark' => $item['remark'] ?? null,
                ]);
            }

            $this->audit('prescription_created', Prescription::class, $prescription->id, [
                'medical_record_id' => $prescription->medical_record_id,
                'doctor_id' => $prescription->doctor_id,
                'type' => $type,
                'items_count' => count($items),
                'valid_until' => $prescription->valid_until->toDateString(),
            ]);

            return $prescription;
        });
    }

    /**
     * 药师审核处方
     *
     * 依据《处方管理办法》第三十六条，药师应当对处方进行审核。
     *
     * @throws \RuntimeException 当处方状态不允许审核
     */
    public function reviewPrescription(int $prescriptionId, int $pharmacistId, bool $approved, string $reason = ''): Prescription
    {
        return DB::transaction(function () use ($prescriptionId, $pharmacistId, $approved, $reason) {
            /** @var Prescription $prescription */
            $prescription = Prescription::lockForUpdate()->findOrFail($prescriptionId);

            if ($prescription->status !== PrescriptionStatus::PENDING) {
                throw new \RuntimeException('当前状态不允许审核');
            }

            $prescription->update([
                'pharmacist_id' => $pharmacistId,
                'status' => $approved ? PrescriptionStatus::APPROVED : PrescriptionStatus::REJECTED,
                'reviewed_at' => now(),
                'reject_reason' => $approved ? null : $reason,
            ]);

            $this->audit(
                $approved ? 'prescription_approved' : 'prescription_rejected',
                Prescription::class,
                $prescription->id,
                [
                    'pharmacist_id' => $pharmacistId,
                    'reason' => $reason,
                ]
            );

            return $prescription;
        });
    }

    /**
     * 电子病历签名归档
     *
     * 依据《电子病历应用管理规范》第十条，电子病历应使用电子签名。
     * 签名后状态变更为 archived，不可再修改。
     *
     * @param int $recordId 病历 ID
     * @param int $doctorId 签名医师 ID
     * @throws \RuntimeException 当病历状态不允许签名
     */
    public function signMedicalRecord(int $recordId, int $doctorId): MedicalRecord
    {
        return DB::transaction(function () use ($recordId, $doctorId) {
            /** @var MedicalRecord $record */
            $record = MedicalRecord::lockForUpdate()->findOrFail($recordId);

            if ($record->status !== MedicalRecordStatus::DRAFT) {
                throw new \RuntimeException('仅草稿状态病历可签名');
            }

            if ($record->doctor_id !== $doctorId) {
                throw new \RuntimeException('仅主治医师可签名');
            }

            // 生成电子签名（HMAC-SHA256，符合《电子签名法》第十三条）
            $signature = hash_hmac('sha256', "record:{$record->id}:doctor:{$doctorId}:time:" . time(), config('app.key'));

            $record->update([
                'status' => MedicalRecordStatus::SIGNED,
                'signed_at' => now(),
            ]);

            $this->audit('medical_record_signed', MedicalRecord::class, $record->id, [
                'doctor_id' => $doctorId,
                'patient_id' => $record->patient_id,
                'diagnosis_code' => $record->diagnosis_code,
                'signature' => $signature,
                'signed_at' => $record->signed_at->toDateTimeString(),
            ]);

            return $record;
        });
    }

    /**
     * 查询患者病历（含权限校验）
     *
     * 依据《医疗机构病历管理规定》第二十八条，医疗机构应当受理患者本人
     * 或其委托代理人复制或查阅病历资料的申请。
     *
     * @param int $patientId 患者ID
     * @param int $currentUserId 当前请求用户ID
     * @param bool $isAuthorized 是否已授权（本人或委托代理人）
     */
    public function getPatientRecords(int $patientId, int $currentUserId, bool $isAuthorized = false): array
    {
        $patient = Patient::findOrFail($patientId);

        // 权限校验：仅患者本人或授权代理人可查阅
        if (!$isAuthorized && $patient->user_id !== $currentUserId) {
            $this->audit('medical_record_access_denied', Patient::class, $patientId, [
                'requester_id' => $currentUserId,
                'patient_user_id' => $patient->user_id,
            ]);
            throw new \RuntimeException('无权查阅该患者病历（《医疗机构病历管理规定》第二十八条）');
        }

        $records = MedicalRecord::where('patient_id', $patientId)
            ->orderByDesc('visit_date')
            ->get();

        $this->audit('medical_record_accessed', Patient::class, $patientId, [
            'requester_id' => $currentUserId,
            'records_count' => $records->count(),
        ]);

        return $records->all();
    }

    /**
     * 生成挂号编号
     */
    private function generateAppointmentNo(): string
    {
        return 'AP' . date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
    }

    /**
     * 记录审计日志
     *
     * 依据《网络安全法》第二十一条，操作日志保留 >= 180 天。
     * 依据等保 2.0 (GB/T 22239)，审计记录不可篡改。
     *
     * @param string $event 事件名
     * @param string $modelType 模型类型
     * @param int $modelId 模型ID
     * @param array<string, mixed> $extra 额外数据
     */
    private function audit(string $event, string $modelType, int $modelId, array $extra = []): void
    {
        if (!class_exists(\Kylin\AuditLog\Models\AuditLog::class)) {
            return;
        }

        try {
            \Kylin\AuditLog\Models\AuditLog::create([
                'event' => $event,
                'model_type' => $modelType,
                'model_id' => $modelId,
                'new_values' => $extra,
                'old_values' => null,
                'user_id' => auth()->id(),
                'ip' => request()?->ip(),
                'url' => request()?->fullUrl(),
                'tag' => 'medical',
            ]);
        } catch (Throwable $e) {
            Log::warning('医疗审计日志写入失败', [
                'error' => $e->getMessage(),
                'event' => $event,
                'model_type' => $modelType,
                'model_id' => $modelId,
            ]);
        }
    }
}
