<?php

namespace Modules\MedDocuments\Services;

use App\System;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Modules\MedDocuments\Entities\MeddocAccessLog;
use Modules\MedDocuments\Entities\MeddocDocument;
use Modules\MedDocuments\Entities\MeddocShare;
use Modules\MedDocuments\Entities\MeddocType;
use Modules\MedDocuments\Entities\MeddocVersion;

/**
 * MedDocuments — the shared documents service (Shifa P2-T2).
 *
 * This is the public attach/retrieve API every other module calls, e.g.:
 *
 *   $doc = app(\Modules\MedDocuments\Services\MedDocuments::class)
 *       ->attach($businessId, 'consent_form', $request->file('doc'), [
 *           'title'       => 'Surgery consent',
 *           'entity_type' => \Modules\ClinicCore\Entities\ClcAppointment::class,
 *           'entity_id'   => $appointment->id,
 *           'contact_id'  => $patientContactId,
 *           'user_id'     => auth()->id(),
 *           'tags'        => ['surgery', 'signed'],
 *       ]);
 *
 * Storage never reimplements any backend/encryption: when SmartStorage is
 * installed we resolve ITS disk (which is transparently swapped to the smart
 * adapter — backends/encryption/ss_access_logs come for free); otherwise the
 * private `meddoc` disk (off the public web root). Every access is logged to
 * meddoc_access_logs and files are served only via controller routes.
 */
class MedDocuments
{
    /*
    |--------------------------------------------------------------------------
    | Disk resolution (reuse-first)
    |--------------------------------------------------------------------------
    */

    /** True when the SmartStorage module is installed for this deployment. */
    public function smartStorageInstalled(): bool
    {
        try {
            return ! empty(System::getProperty('smartstorage_version'));
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * Name of the disk we store on — the private `meddoc` disk by default
     * (rooted at storage/app/meddocs, off the public web root).
     *
     * SmartStorage's target disk is used ONLY when it points at a disk whose
     * root is private: on this core the plain `local` disk is rooted at
     * public_path('uploads') (web-reachable), and an installed-but-inactive
     * SmartStorage does not intercept writes there — medical documents must
     * never land under the public root. An explicit
     * config('meddocuments.storage_disk') override always wins.
     */
    public function diskName(): string
    {
        $override = config('meddocuments.storage_disk');
        if (! empty($override)) {
            return (string) $override;
        }

        if ($this->smartStorageInstalled()) {
            $target = (string) config('smartstorage.target_disk', '');
            if ($target !== '' && ! $this->diskIsPubliclyRooted($target)) {
                return $target;
            }
        }

        return (string) config('meddocuments.fallback_disk', 'meddoc');
    }

    /** True when the given disk's root lives under the public web root. */
    protected function diskIsPubliclyRooted(string $disk): bool
    {
        $root = (string) config("filesystems.disks.{$disk}.root", '');
        if ($root === '') {
            return true; // unknown root — refuse rather than risk exposure
        }

        $normalized = str_replace('\\', '/', $root);
        $publicRoot = str_replace('\\', '/', public_path());

        return str_starts_with($normalized, $publicRoot);
    }

    public function disk(?string $name = null): Filesystem
    {
        return Storage::disk($name ?: $this->diskName());
    }

    /*
    |--------------------------------------------------------------------------
    | Attach / version
    |--------------------------------------------------------------------------
    */

    /**
     * Attach a new document (creates the header + version 1).
     *
     * @param  array  $opts  title, entity_type, entity_id, contact_id, tags[],
     *                       user_id, notes
     */
    public function attach(int $businessId, string $docTypeKey, UploadedFile $file, array $opts = []): MeddocDocument
    {
        $type = $this->resolveType($businessId, $docTypeKey);
        $userId = $opts['user_id'] ?? auth()->id();

        return DB::transaction(function () use ($businessId, $type, $file, $opts, $userId) {
            $doc = MeddocDocument::create([
                'business_id' => $businessId,
                'doc_type_id' => $type->id,
                'title'       => $opts['title'] ?? $file->getClientOriginalName(),
                'entity_type' => $opts['entity_type'] ?? null,
                'entity_id'   => $opts['entity_id'] ?? null,
                'contact_id'  => $opts['contact_id'] ?? null,
                'tags'        => $opts['tags'] ?? null,
                'status'      => 'active',
                'created_by'  => $userId,
            ]);

            $version = $this->writeVersion($doc, $file, $userId, $opts['notes'] ?? null, 1);

            $doc->current_version_id = $version->id;
            $doc->save();

            $this->log($doc, 'upload', $version->id, $userId);

            return $doc->fresh(['currentVersion', 'type']);
        });
    }

    /**
     * Add a new version to an existing document. Returns the new version.
     */
    public function addVersion(MeddocDocument $document, UploadedFile $file, $user = null, ?string $notes = null): MeddocVersion
    {
        $userId = is_object($user) ? ($user->id ?? null) : $user;
        $userId = $userId ?: auth()->id();

        return DB::transaction(function () use ($document, $file, $userId, $notes) {
            $nextNo = (int) $document->versions()->max('version_no') + 1;

            $version = $this->writeVersion($document, $file, $userId, $notes, $nextNo);

            $document->current_version_id = $version->id;
            $document->save();

            $this->log($document, 'upload', $version->id, $userId);

            return $version;
        });
    }

    /**
     * Physically store one file for a document and record a meddoc_versions row.
     * Path: meddocs/{business_id}/{document_id}/v{n}_{sanitized_filename}
     */
    protected function writeVersion(MeddocDocument $document, UploadedFile $file, $userId, ?string $notes, int $versionNo): MeddocVersion
    {
        $disk = $this->diskName();
        $original = $file->getClientOriginalName();
        $safe = $this->sanitizeFilename($original);

        $dir = trim((string) config('meddocuments.path_prefix', 'meddocs'), '/')
            . '/' . $document->business_id . '/' . $document->id;
        $path = $dir . '/v' . $versionNo . '_' . $safe;

        // getRealPath()->contents keeps the whole flow on the resolved disk
        // (so SmartStorage's encryption/logging applies transparently).
        $stream = fopen($file->getRealPath(), 'rb');
        $this->disk($disk)->put($path, $stream);
        if (is_resource($stream)) {
            fclose($stream);
        }

        $checksum = @hash_file('sha256', $file->getRealPath()) ?: null;

        return MeddocVersion::create([
            'document_id'   => $document->id,
            'version_no'    => $versionNo,
            'file_path'     => $path,
            'original_name' => $original,
            'mime'          => $file->getClientMimeType() ?: $file->getMimeType(),
            'size_bytes'    => $file->getSize() ?: 0,
            'storage_disk'  => $disk,
            'checksum'      => $checksum,
            'uploaded_by'   => $userId,
            'notes'         => $notes,
            'created_at'    => now(),
        ]);
    }

    /*
    |--------------------------------------------------------------------------
    | Retrieve / stream
    |--------------------------------------------------------------------------
    */

    /**
     * Resolve a version to stream (a specific one, or the document's current).
     */
    public function versionToServe(MeddocDocument $document, $versionId = null): ?MeddocVersion
    {
        if (! empty($versionId)) {
            return $document->versions()->whereKey($versionId)->first();
        }

        if ($document->current_version_id) {
            return MeddocVersion::find($document->current_version_id);
        }

        return $document->versions()->first();
    }

    /**
     * Read the raw stored bytes of a version off its recorded disk.
     */
    public function bytes(MeddocVersion $version): ?string
    {
        $disk = $version->storage_disk ?: $this->diskName();
        try {
            if (! $this->disk($disk)->exists($version->file_path)) {
                return null;
            }

            return $this->disk($disk)->get($version->file_path);
        } catch (\Throwable $e) {
            \Log::warning('MedDocuments read failed: ' . $e->getMessage());

            return null;
        }
    }

    /**
     * Return a streamed download response for a version (raw bytes). Logs the
     * given action. Used by the FileController for both audited download and
     * (perm-gated) original download.
     */
    public function stream(MeddocDocument $document, $versionId = null, string $logAction = 'view', $userId = null)
    {
        $version = $this->versionToServe($document, $versionId);
        if (! $version) {
            abort(404);
        }

        $bytes = $this->bytes($version);
        if ($bytes === null) {
            abort(404);
        }

        $this->log($document, $logAction, $version->id, $userId ?: auth()->id());

        return response($bytes, 200, [
            'Content-Type'        => $version->mime ?: 'application/octet-stream',
            'Content-Length'      => strlen($bytes),
            'Content-Disposition' => 'inline; filename="' . addslashes($version->original_name) . '"',
            'Cache-Control'       => 'private, no-store',
        ]);
    }

    /*
    |--------------------------------------------------------------------------
    | Query helpers
    |--------------------------------------------------------------------------
    */

    /**
     * All active documents attached to a given entity (any module). Optionally
     * scoped to a business.
     */
    public function for(string $entityType, $entityId, ?int $businessId = null)
    {
        $q = MeddocDocument::query()
            ->where('entity_type', $entityType)
            ->where('entity_id', $entityId)
            ->where('status', '!=', 'deleted');

        if ($businessId !== null) {
            $q->where('business_id', $businessId);
        }

        return $q->with(['type', 'currentVersion'])->orderByDesc('updated_at');
    }

    /*
    |--------------------------------------------------------------------------
    | Sharing
    |--------------------------------------------------------------------------
    */

    /**
     * Create a secure, expiring share link for a document. Returns the
     * MeddocShare (call ->token / shareUrl()).
     *
     * @param  int|null  $days  days until expiry (null = no expiry)
     */
    public function share(MeddocDocument $document, ?int $days, $user = null): MeddocShare
    {
        $userId = is_object($user) ? ($user->id ?? null) : $user;
        $userId = $userId ?: auth()->id();

        $share = MeddocShare::create([
            'business_id' => $document->business_id,
            'document_id' => $document->id,
            'token'       => $this->newToken(),
            'expires_at'  => $days ? now()->addDays($days) : null,
            'created_by'  => $userId,
        ]);

        $this->log($document, 'share', $document->current_version_id, $userId);

        return $share;
    }

    public function shareUrl(MeddocShare $share): string
    {
        return route('meddoc.shared.show', ['token' => $share->token]);
    }

    protected function newToken(): string
    {
        do {
            $token = Str::random(64);
        } while (MeddocShare::where('token', $token)->exists());

        return $token;
    }

    /*
    |--------------------------------------------------------------------------
    | Audit
    |--------------------------------------------------------------------------
    */

    /**
     * Insert an audit row. $userId null = public (share-link) access.
     */
    public function log(MeddocDocument $document, string $action, $versionId = null, $userId = null): void
    {
        if (! in_array($action, MeddocAccessLog::ACTIONS, true)) {
            return;
        }

        try {
            MeddocAccessLog::create([
                'business_id' => $document->business_id,
                'document_id' => $document->id,
                'version_id'  => $versionId,
                'user_id'     => $userId,
                'action'      => $action,
                'ip'          => request()->ip(),
                'user_agent'  => substr((string) request()->userAgent(), 0, 255),
                'created_at'  => now(),
            ]);
        } catch (\Throwable $e) {
            // Never let audit logging break the primary operation.
            \Log::warning('MedDocuments audit log failed: ' . $e->getMessage());
        }
    }

    /*
    |--------------------------------------------------------------------------
    | Internals
    |--------------------------------------------------------------------------
    */

    /**
     * Resolve a doc-type by key for a business (custom type wins over system).
     */
    protected function resolveType(int $businessId, string $docTypeKey): MeddocType
    {
        $type = MeddocType::forBusiness($businessId)
            ->where('type_key', $docTypeKey)
            ->orderByRaw('business_id IS NULL') // prefer the tenant's own row
            ->first();

        if (! $type) {
            // Auto-create a custom type so an unknown key never blocks an attach.
            $type = MeddocType::create([
                'business_id' => $businessId,
                'type_key'    => $docTypeKey,
                'label'       => Str::title(str_replace('_', ' ', $docTypeKey)),
                'is_active'   => true,
            ]);
        }

        return $type;
    }

    protected function sanitizeFilename(string $name): string
    {
        $ext = pathinfo($name, PATHINFO_EXTENSION);
        $base = pathinfo($name, PATHINFO_FILENAME);
        $base = Str::slug($base) ?: 'file';
        $base = substr($base, 0, 120);

        return $ext ? $base . '.' . strtolower($ext) : $base;
    }
}
