<?php

namespace Modules\ClinicCore\Entities;

use Illuminate\Database\Eloquent\Model;

/**
 * Append-only chain of daily SHA-256 seals over the controlled-drug register.
 *
 *   hash = SHA-256( prev_hash + payload )
 *
 * payload is a stable, line-by-line serialisation of every movement row whose
 * created_at falls on that seal_date. Any later mutation of those rows (or
 * insertion of a back-dated row) makes the recomputed hash mismatch the
 * stored one, and verifyChain() reports the first breach.
 */
class ClcControlledDailySeal extends Model
{
    protected $table = 'clc_controlled_daily_seals';
    protected $guarded = ['id'];

    protected $casts = [
        'seal_date' => 'date',
        'sealed_at' => 'datetime',
    ];

    /** Hash one movement row to a stable string used inside the day's payload. */
    public static function rowFingerprint(ClcControlledMovement $m): string
    {
        return implode('|', [
            $m->id, $m->serial_no, $m->medicine_id,
            $m->direction, number_format((float) $m->qty, 3, '.', ''),
            (string) $m->batch_no, (string) $m->expiry_date,
            number_format((float) $m->balance_after, 3, '.', ''),
            $m->reason_code, (string) $m->patient_id, (string) $m->contact_id,
            (string) $m->appointment_id, (string) $m->doctor_id,
            (string) $m->witness_user_id, (string) $m->source_type,
            (string) $m->source_id, (string) $m->source_line_id,
            optional($m->created_at)->format('Y-m-d H:i:s'),
        ]);
    }

    public static function computeDayHash(int $businessId, string $date, ?string $prevHash): array
    {
        $rows = ClcControlledMovement::where('business_id', $businessId)
            ->whereDate('created_at', $date)
            ->orderBy('id')->get();

        $payload = ($prevHash ?? '') . "\n";
        foreach ($rows as $m) {
            $payload .= self::rowFingerprint($m) . "\n";
        }
        return [
            'hash'    => hash('sha256', $payload),
            'count'   => $rows->count(),
            'first'   => $rows->first()->id ?? null,
            'last'    => $rows->last()->id ?? null,
        ];
    }

    /**
     * Seal every business-day older than today that doesn't have a seal yet.
     * Returns an array of [date, hash, count] seals just created.
     */
    public static function sealOutstanding(int $businessId): array
    {
        $today = now()->toDateString();
        $dates = ClcControlledMovement::where('business_id', $businessId)
            ->whereDate('created_at', '<', $today)
            ->selectRaw('DATE(created_at) as d')
            ->groupBy('d')->orderBy('d')->pluck('d')->all();

        $sealed = static::where('business_id', $businessId)->pluck('seal_date')->map(fn($d) => $d->toDateString())->all();
        $missing = array_values(array_diff($dates, $sealed));

        $results = [];
        $prevHash = static::where('business_id', $businessId)->orderByDesc('seal_date')->value('hash');

        foreach ($missing as $date) {
            $computed = self::computeDayHash($businessId, $date, $prevHash);
            $seal = static::create([
                'business_id'       => $businessId,
                'seal_date'         => $date,
                'movement_count'    => $computed['count'],
                'first_movement_id' => $computed['first'],
                'last_movement_id'  => $computed['last'],
                'prev_hash'         => $prevHash,
                'hash'              => $computed['hash'],
                'sealed_at'         => now(),
                'sealed_by'         => auth()->id(),
            ]);
            $prevHash = $seal->hash;
            $results[] = ['date' => $date, 'hash' => $seal->hash, 'count' => $computed['count']];
        }
        return $results;
    }

    /**
     * Recompute every existing seal and compare to its stored hash. Returns
     * either ['ok'=>true,'seals'=>N] or ['ok'=>false,'first_breach'=>...].
     */
    public static function verifyChain(int $businessId): array
    {
        $seals = static::where('business_id', $businessId)
            ->orderBy('seal_date')->orderBy('id')->get();
        if ($seals->isEmpty()) return ['ok' => true, 'seals' => 0];

        $prevHash = null;
        foreach ($seals as $s) {
            $computed = self::computeDayHash($businessId, $s->seal_date->toDateString(), $prevHash);
            if ($computed['hash'] !== $s->hash) {
                return [
                    'ok' => false,
                    'first_breach' => [
                        'date'          => $s->seal_date->toDateString(),
                        'stored_hash'   => $s->hash,
                        'recomputed_hash' => $computed['hash'],
                        'sealed_at'     => optional($s->sealed_at)->format('Y-m-d H:i'),
                        'row_count'     => $s->movement_count,
                    ],
                ];
            }
            $prevHash = $s->hash;
        }
        return ['ok' => true, 'seals' => $seals->count(), 'latest' => $prevHash];
    }
}
