<?php

namespace App\Services\Admin\Notice;

use App\Models\GcBoard;
use App\Repositories\Admin\Notice\AdminNoticeRepository;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use App\Services\Admin\AdminAuditService;

final class AdminNoticeService
{
    private const GUBUN = 'notice';
    private const DISK  = 'public';
    private const DIR   = 'bbs';

    public function __construct(
        private readonly AdminNoticeRepository $repo,
        private readonly AdminAuditService $audit,
    ) {}

    public function paginate(array $filters, int $perPage = 15)
    {
        // paginator 링크 쿼리스트링 유지
        return $this->repo->paginate($filters, $perPage)->withQueryString();
    }

    public function get(int $id): GcBoard
    {
        return $this->repo->findOrFailForEdit($id);
    }

    public function create(array $data, ?UploadedFile $file1, ?UploadedFile $file2, int $adminId): int
    {
        // 파일은 먼저 저장(성공 시 DB 반영), 예외 시 롤백+파일 삭제
        $new1 = $file1 ? $this->storeFile($file1) : null;
        $new2 = $file2 ? $this->storeFile($file2) : null;

        try {
            return $this->repo->transaction(function () use ($data, $adminId, $new1, $new2) {

                $wantPinned = !empty($data['first_sign']);
                $firstSign  = 0;

                if ($wantPinned) {
                    $max = $this->repo->maxFirstSignForUpdate(); // lock 포함
                    $firstSign = $max + 1; // ✅ 상단공지 순번(큰 값이 먼저)
                }

                $content = (string)($data['content'] ?? '');

                $payload = [
                    'gubun'      => self::GUBUN,
                    'subject'    => (string)($data['subject'] ?? ''),
                    'content'    => $content,
                    'admin_num'  => $adminId,
                    'regdate'    => now()->format('Y-m-d H:i:s'),
                    'hiding'     => 'N', // 기본 노출
                    'first_sign' => $firstSign,
                    'link_01'    => $this->nullIfBlank($data['link_01'] ?? null),
                    'link_02'    => $this->nullIfBlank($data['link_02'] ?? null),
                    'hit'        => 0,
                ];

                if ($new1) $payload['file_01'] = $new1;
                if ($new2) $payload['file_02'] = $new2;

                $row = $this->repo->create($payload);
                $id  = (int) $row->getKey();

                // ✅ 감사로그: 성공시에만, 실패해도 본 로직 영향 X
                if ($id > 0) {
                    try {
                        $req = request();
                        $this->audit->log(
                            actorAdminId: $adminId,
                            action: 'admin.notice.create',
                            targetType: 'notice',
                            targetId: $id,
                            before: null,
                            after: [
                                'id' => $id,
                                'gubun' => $payload['gubun'],
                                'subject' => $payload['subject'],
                                'hiding' => $payload['hiding'],
                                'first_sign' => $payload['first_sign'],
                                'link_01' => $payload['link_01'],
                                'link_02' => $payload['link_02'],
                                'file_01' => $payload['file_01'] ?? null,
                                'file_02' => $payload['file_02'] ?? null,
                                'content_len' => mb_strlen($content),
                                'content_sha256' => hash('sha256', $content),
                                'regdate' => $payload['regdate'],
                                'admin_num' => $payload['admin_num'],
                            ],
                            ip: (string)($req?->ip() ?? ''),
                            ua: (string)($req?->userAgent() ?? ''),
                        );
                    } catch (\Throwable $ignored) {}
                }

                return $id;
            });
        } catch (\Throwable $e) {
            // DB 실패 시 새로 저장한 파일 제거
            $this->deletePhysicalFiles([$new1, $new2]);
            throw $e;
        }
    }


    public function update(int $id, array $data, ?UploadedFile $file1, ?UploadedFile $file2): void
    {
        // 새 파일 먼저 저장
        $new1 = $file1 ? $this->storeFile($file1) : null;
        $new2 = $file2 ? $this->storeFile($file2) : null;

        $oldToDelete = [];

        try {
            $oldToDelete = (array) $this->repo->transaction(function () use ($id, $data, $new1, $new2) {

                $row = $this->repo->lockForUpdate($id);

                // ✅ 감사로그 before(업데이트 전)
                $beforeContent = (string)($row->content ?? '');
                $beforeAudit = [
                    'id' => (int)$id,
                    'gubun' => (string)($row->gubun ?? self::GUBUN),
                    'subject' => (string)($row->subject ?? ''),
                    'hiding' => (string)($row->hiding ?? ''),
                    'first_sign' => (int)($row->first_sign ?? 0),
                    'link_01' => (string)($row->link_01 ?? ''),
                    'link_02' => (string)($row->link_02 ?? ''),
                    'file_01' => (string)($row->file_01 ?? ''),
                    'file_02' => (string)($row->file_02 ?? ''),
                    'content_len' => mb_strlen($beforeContent),
                    'content_sha256' => hash('sha256', $beforeContent),
                ];

                $hiding = !empty($data['hiding']) ? 'Y' : 'N';

                // ✅ first_sign 로직 유지
                $wantPinned = !empty($data['first_sign']);
                $firstSign  = (int)($row->first_sign ?? 0);

                if ($wantPinned) {
                    if ($firstSign <= 0) {
                        $firstSign = $this->repo->maxFirstSignForUpdate() + 1;
                    }
                } else {
                    $firstSign = 0;
                }

                $content = (string)($data['content'] ?? '');

                $payload = [
                    'subject'    => (string)($data['subject'] ?? ''),
                    'content'    => $content,
                    'link_01'    => $this->nullIfBlank($data['link_01'] ?? null),
                    'link_02'    => $this->nullIfBlank($data['link_02'] ?? null),
                    'hiding'     => $hiding,
                    'first_sign' => $firstSign,
                ];

                $oldFiles = [];

                if ($new1) {
                    $oldFiles[] = (string)($row->file_01 ?? '');
                    $payload['file_01'] = $new1;
                }

                if ($new2) {
                    $oldFiles[] = (string)($row->file_02 ?? '');
                    $payload['file_02'] = $new2;
                }

                $this->repo->update($row, $payload);

                // ✅ 감사로그: 성공시에만, 실패해도 본 로직 영향 X
                try {
                    $actorAdminId = (int)(auth('admin')->id() ?? 0);
                    if ($actorAdminId > 0) {
                        $req = request();

                        $afterAudit = $beforeAudit;
                        $afterAudit['subject'] = $payload['subject'];
                        $afterAudit['hiding'] = $payload['hiding'];
                        $afterAudit['first_sign'] = $payload['first_sign'];
                        $afterAudit['link_01'] = (string)($payload['link_01'] ?? '');
                        $afterAudit['link_02'] = (string)($payload['link_02'] ?? '');
                        if (isset($payload['file_01'])) $afterAudit['file_01'] = (string)$payload['file_01'];
                        if (isset($payload['file_02'])) $afterAudit['file_02'] = (string)$payload['file_02'];
                        $afterAudit['content_len'] = mb_strlen($content);
                        $afterAudit['content_sha256'] = hash('sha256', $content);

                        $this->audit->log(
                            actorAdminId: $actorAdminId,
                            action: 'admin.notice.update',
                            targetType: 'notice',
                            targetId: (int)$id,
                            before: $beforeAudit,
                            after: $afterAudit,
                            ip: (string)($req?->ip() ?? ''),
                            ua: (string)($req?->userAgent() ?? ''),
                        );
                    }
                } catch (\Throwable $ignored) {}

                // 트랜잭션 밖에서 삭제할 이전 파일명 반환
                return $oldFiles;
            });
        } catch (\Throwable $e) {
            // DB 실패 시 새로 저장한 파일 제거
            $this->deletePhysicalFiles([$new1, $new2]);
            throw $e;
        }

        // ✅ 커밋 이후에만 기존 파일 삭제
        $this->deletePhysicalFiles($oldToDelete);
    }


    public function delete(int $id): void
    {
        $files = (array) $this->repo->transaction(function () use ($id) {
            $row = $this->repo->lockForUpdate($id);

            $beforeContent = (string)($row->content ?? '');

            $files = [
                (string)($row->file_01 ?? ''),
                (string)($row->file_02 ?? ''),
            ];

            $this->repo->delete($row);

            // ✅ 감사로그: 성공시에만, 실패해도 본 로직 영향 X
            try {
                $actorAdminId = (int)(auth('admin')->id() ?? 0);
                if ($actorAdminId > 0) {
                    $req = request();

                    $this->audit->log(
                        actorAdminId: $actorAdminId,
                        action: 'admin.notice.delete',
                        targetType: 'notice',
                        targetId: (int)$id,
                        before: [
                            'id' => (int)$id,
                            'gubun' => (string)($row->gubun ?? self::GUBUN),
                            'subject' => (string)($row->subject ?? ''),
                            'hiding' => (string)($row->hiding ?? ''),
                            'first_sign' => (int)($row->first_sign ?? 0),
                            'link_01' => (string)($row->link_01 ?? ''),
                            'link_02' => (string)($row->link_02 ?? ''),
                            'file_01' => (string)($row->file_01 ?? ''),
                            'file_02' => (string)($row->file_02 ?? ''),
                            'content_len' => mb_strlen($beforeContent),
                            'content_sha256' => hash('sha256', $beforeContent),
                        ],
                        after: null,
                        ip: (string)($req?->ip() ?? ''),
                        ua: (string)($req?->userAgent() ?? ''),
                    );
                }
            } catch (\Throwable $ignored) {}

            return $files;
        });

        $this->deletePhysicalFiles($files);
    }


    public function download(int $id, int $slot): array
    {
        $row = $this->repo->findOrFailForEdit($id);

        $file = $slot === 1
            ? (string)($row->file_01 ?? '')
            : (string)($row->file_02 ?? '');

        $file = trim($file);
        if ($file === '') return ['ok' => false, 'path' => '', 'name' => ''];

        $path = self::DIR.'/'.basename($file);
        if (!Storage::disk(self::DISK)->exists($path)) {
            return ['ok' => false, 'path' => '', 'name' => ''];
        }

        return ['ok' => true, 'path' => $path, 'name' => $file];
    }

    private function storeFile(UploadedFile $file): string
    {
        $orig = $file->getClientOriginalName();
        $safe = $this->safeFilename($orig);

        $base = pathinfo($safe, PATHINFO_FILENAME);
        $ext  = pathinfo($safe, PATHINFO_EXTENSION);
        $name = $safe;

        // 충돌 방지
        $i = 0;
        while (Storage::disk(self::DISK)->exists(self::DIR.'/'.$name)) {
            $i++;
            $suffix = date('YmdHis').'_'.mt_rand(1000, 9999).'_'.$i;
            $name = $base.'_'.$suffix.($ext ? '.'.$ext : '');
        }

        Storage::disk(self::DISK)->putFileAs(self::DIR, $file, $name);
        return $name;
    }

    private function deletePhysicalFiles(array $files): void
    {
        foreach ($files as $f) {
            $f = trim((string)$f);
            if ($f === '') continue;

            $path = self::DIR.'/'.$f;
            if (Storage::disk(self::DISK)->exists($path)) {
                Storage::disk(self::DISK)->delete($path);
            }
        }
    }

    private function safeFilename(string $name): string
    {
        $name = trim($name);
        $name = preg_replace('/[^\pL\pN\.\-\_\s]/u', '', $name) ?: 'file';
        $name = preg_replace('/\s+/', '_', $name);
        $name = preg_replace('/_+/', '_', $name);
        return mb_substr($name, 0, 180);
    }

    private function nullIfBlank($v): ?string
    {
        $v = trim((string)($v ?? ''));
        return $v === '' ? null : $v;
    }
}
