<?php

declare(strict_types=1);

$appRoot = (string) (getenv('GTSIT_APP_ROOT') ?: dirname(__DIR__));
require $appRoot . '/app/bootstrap.php';

use App\Repositories\PostRepository;
use App\Repositories\InquiryRepository;
use App\Repositories\AdminRepository;
use App\Auth\AdminAuth;
use App\Repositories\SiteSettingRepository;

$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$basePath = (string) config('app.base_path', '');
if ($basePath !== '') {
    if ($path !== $basePath && !str_starts_with($path, $basePath . '/')) {
        http_response_code(404);
        exit;
    }
    $path = substr($path, strlen($basePath)) ?: '/';
}
$path = rtrim($path, '/') ?: '/';
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');

if (PHP_SAPI === 'cli-server' && $path !== '/' && is_file(__DIR__ . $path)) {
    return false;
}

$posts = [];
$databaseReady = true;
try {
    $postRepository = new PostRepository();
} catch (Throwable) {
    $postRepository = null;
    $databaseReady = false;
}

if ($path === '/' && $method === 'GET') {
    if ($postRepository !== null) {
        $posts = $postRepository->latest(3);
    }
    render('home', [
        'title' => '사무실 통신 인프라 전문',
        'description' => '네트워크, 키폰, CCTV 설계부터 시공과 유지보수까지 책임지는 (주)지티에스정보통신입니다.',
        'posts' => $posts,
    ]);
    exit;
}

if ($path === '/blog' && $method === 'GET') {
    $page = max(1, (int) ($_GET['page'] ?? 1));
    $category = trim((string) ($_GET['category'] ?? ''));
    $result = ['items' => [], 'total' => 0];
    if ($postRepository !== null) {
        $result = $postRepository->paginate($page, 6, $category ?: null);
    }
    render('blog/index', [
        'title' => '시공 이야기와 정보',
        'description' => '네트워크, 키폰, CCTV 시공 사례와 장비 선택 정보를 확인하세요.',
        'posts' => $result['items'],
        'total' => $result['total'],
        'page' => $page,
        'perPage' => 6,
        'category' => $category,
    ]);
    exit;
}

if (preg_match('#^/blog/([a-z0-9-]+)$#', $path, $matches) === 1 && $method === 'GET') {
    $post = $postRepository?->findPublishedBySlug($matches[1]);
    if ($post === null) {
        http_response_code(404);
        render('errors/404', ['title' => '게시글을 찾을 수 없습니다']);
        exit;
    }
    render('blog/show', [
        'title' => $post['title'],
        'description' => $post['excerpt'],
        'post' => $post,
    ]);
    exit;
}

if ($path === '/contact' && $method === 'GET') {
    render('contact', [
        'title' => '1:1 비공개 상담 신청',
        'description' => '네트워크, 키폰, CCTV 구축과 유지보수 상담을 신청하세요.',
        'errors' => [],
    ]);
    exit;
}

if ($path === '/contact' && $method === 'POST') {
    verify_csrf();
    $allowedTypes = ['네트워크 구축', '키폰시스템', 'CCTV 설치', '시공·유지보수', '기타 문의'];
    $data = [
        'name' => trim((string) ($_POST['name'] ?? '')),
        'phone' => trim((string) ($_POST['phone'] ?? '')),
        'email' => trim((string) ($_POST['email'] ?? '')),
        'type' => trim((string) ($_POST['type'] ?? '')),
        'title' => trim((string) ($_POST['title'] ?? '')),
        'body' => trim((string) ($_POST['body'] ?? '')),
        'password' => (string) ($_POST['password'] ?? ''),
    ];
    $errors = [];
    if ($data['name'] === '' || mb_strlen($data['name']) > 120) $errors[] = '이름 또는 회사명을 확인해 주세요.';
    if (!preg_match('/^(?:02-\d{3,4}-\d{4}|0\d{2}-\d{3,4}-\d{4}|050\d-\d{4}-\d{4}|1\d{3}-\d{4})$/D', $data['phone'])) $errors[] = '연락처 형식을 확인해 주세요.';
    if ($data['email'] !== '' && filter_var($data['email'], FILTER_VALIDATE_EMAIL) === false) $errors[] = '이메일 형식을 확인해 주세요.';
    if (!in_array($data['type'], $allowedTypes, true)) $errors[] = '문의 유형을 선택해 주세요.';
    if ($data['title'] === '' || mb_strlen($data['title']) > 200) $errors[] = '제목을 200자 이내로 입력해 주세요.';
    if ($data['body'] === '' || mb_strlen($data['body']) > 5000) $errors[] = '문의 내용을 5,000자 이내로 입력해 주세요.';
    if (!preg_match('/^[0-9]{4}$/D', $data['password'])) $errors[] = '조회 PIN은 숫자 4자리로 입력해 주세요.';
    if (($_POST['consent'] ?? '') !== '1') $errors[] = '개인정보 수집·이용 동의가 필요합니다.';

    $lastSubmit = (int) ($_SESSION['last_inquiry_submit'] ?? 0);
    if (time() - $lastSubmit < 20) $errors[] = '중복 접수를 방지하기 위해 잠시 후 다시 시도해 주세요.';

    if ($errors) {
        http_response_code(422);
        render('contact', [
            'title' => '1:1 비공개 상담 신청',
            'description' => '네트워크, 키폰, CCTV 구축과 유지보수 상담을 신청하세요.',
            'errors' => $errors,
        ]);
        exit;
    }

    try {
        $ticket = (new InquiryRepository())->create($data);
        $_SESSION['last_inquiry_submit'] = time();
        $_SESSION['submitted_ticket'] = $ticket;
        redirect('/contact/success');
    } catch (Throwable) {
        http_response_code(503);
        render('contact', [
            'title' => '1:1 비공개 상담 신청',
            'description' => '네트워크, 키폰, CCTV 구축과 유지보수 상담을 신청하세요.',
            'errors' => ['현재 온라인 접수가 원활하지 않습니다. 대표번호로 문의해 주세요.'],
        ]);
        exit;
    }
}

if ($path === '/contact/success' && $method === 'GET') {
    $ticket = $_SESSION['submitted_ticket'] ?? null;
    if (!is_string($ticket) || $ticket === '') redirect('/contact');
    unset($_SESSION['submitted_ticket']);
    render('inquiries/success', ['title' => '문의 접수 완료', 'ticket' => $ticket]);
    exit;
}

if ($path === '/inquiry/lookup' && $method === 'GET') {
    render('inquiries/lookup', [
        'title' => '문의 접수 확인',
        'description' => '접수번호와 숫자 4자리 조회 PIN으로 상담 처리 상태를 확인하세요.',
        'ticket' => trim((string) ($_GET['ticket'] ?? '')),
        'error' => '',
    ]);
    exit;
}

if ($path === '/inquiry/lookup' && $method === 'POST') {
    verify_csrf();
    $window = $_SESSION['lookup_rate'] ?? ['started' => time(), 'attempts' => 0];
    if (time() - (int) $window['started'] > 900) $window = ['started' => time(), 'attempts' => 0];
    if ((int) $window['attempts'] >= 5) {
        http_response_code(429);
        render('inquiries/lookup', ['title'=>'문의 접수 확인','ticket'=>'','error'=>'잠시 후 다시 시도해 주세요.']);
        exit;
    }
    $ticket = strtoupper(trim((string) ($_POST['ticket'] ?? '')));
    $password = (string) ($_POST['password'] ?? '');
    $inquiry = null;
    $inquiryRepository = null;
    try {
        $inquiryRepository = new InquiryRepository();
        $inquiry = $inquiryRepository->findForLookup($ticket);
    } catch (Throwable) {}

    if ($inquiry !== null && !empty($inquiry['lookup_locked_until'])) {
        if (strtotime((string) $inquiry['lookup_locked_until']) > time()) {
            http_response_code(429);
            render('inquiries/lookup', ['title'=>'문의 접수 확인','ticket'=>$ticket,'error'=>'조회 시도가 제한되었습니다. 15분 후 다시 시도해 주세요.']);
            exit;
        }
        try { $inquiryRepository?->resetLookupLimit((int) $inquiry['id']); } catch (Throwable) {}
    }

    $validPin = preg_match('/^[0-9]{4}$/D', $password) === 1;
    if ($inquiry === null || !$validPin || !password_verify($password, $inquiry['password_hash'])) {
        $window['attempts']++;
        $_SESSION['lookup_rate'] = $window;
        $locked = false;
        if ($inquiry !== null && $inquiryRepository instanceof InquiryRepository) {
            try { $locked = $inquiryRepository->recordLookupFailure((int) $inquiry['id']); } catch (Throwable) {}
        }
        http_response_code($locked ? 429 : 422);
        render('inquiries/lookup', [
            'title'=>'문의 접수 확인',
            'ticket'=>$ticket,
            'error'=>$locked ? '조회 시도가 제한되었습니다. 15분 후 다시 시도해 주세요.' : '접수번호 또는 조회 PIN이 올바르지 않습니다.',
        ]);
        exit;
    }
    try { $inquiryRepository?->resetLookupLimit((int) $inquiry['id']); } catch (Throwable) {}
    unset($_SESSION['lookup_rate'], $inquiry['id'], $inquiry['password_hash'], $inquiry['lookup_failed_attempts'], $inquiry['lookup_locked_until']);
    render('inquiries/show', ['title'=>'문의 처리 상태','description'=>'비공개 상담 처리 상태입니다.','inquiry'=>$inquiry]);
    exit;
}

if ($path === '/privacy' && $method === 'GET') {
    render('privacy', [
        'title' => '개인정보처리방침',
        'description' => '(주)지티에스정보통신 개인정보처리방침입니다.',
    ]);
    exit;
}

if ($path === '/admin/setup' && $method === 'GET') {
    try {
        $adminRepository = new AdminRepository();
        if ($adminRepository->adminCount() > 0) redirect('/admin/login');
    } catch (Throwable) {
        http_response_code(503);
        render('errors/404', ['title'=>'관리자 등록을 준비할 수 없습니다'], 'layouts/admin');
        exit;
    }

    $token = (string) ($_GET['token'] ?? '');
    $tokenFile = (string) (getenv('GTSIT_ADMIN_SETUP_TOKEN_FILE') ?: '');
    if ($token !== '' && $tokenFile !== '' && is_readable($tokenFile) && time() - (int) filemtime($tokenFile) <= 86400) {
        $storedHash = trim((string) file_get_contents($tokenFile));
        if ($storedHash !== '' && hash_equals($storedHash, hash('sha256', $token))) {
            session_regenerate_id(true);
            $_SESSION['admin_setup_verified'] = time();
            redirect('/admin/setup');
        }
    }

    if (time() - (int) ($_SESSION['admin_setup_verified'] ?? 0) > 900) {
        unset($_SESSION['admin_setup_verified']);
        http_response_code(404);
        render('errors/404', ['title'=>'페이지를 찾을 수 없습니다'], 'layouts/admin');
        exit;
    }

    render('admin/setup', [
        'title'=>'관리자 계정 등록',
        'username'=>(string) (getenv('GTSIT_INITIAL_ADMIN_USERNAME') ?: 'gts0201'),
        'displayName'=>'관리자',
        'errors'=>[],
    ], 'layouts/admin');
    exit;
}

if ($path === '/admin/setup' && $method === 'POST') {
    verify_csrf();
    if (time() - (int) ($_SESSION['admin_setup_verified'] ?? 0) > 900) {
        unset($_SESSION['admin_setup_verified']);
        http_response_code(404);
        render('errors/404', ['title'=>'페이지를 찾을 수 없습니다'], 'layouts/admin');
        exit;
    }

    $username = (string) (getenv('GTSIT_INITIAL_ADMIN_USERNAME') ?: 'gts0201');
    $displayName = trim((string) ($_POST['display_name'] ?? ''));
    $password = (string) ($_POST['password'] ?? '');
    $passwordConfirmation = (string) ($_POST['password_confirmation'] ?? '');
    $errors = [];
    if ($displayName === '' || mb_strlen($displayName) > 100) $errors[] = '표시 이름을 100자 이내로 입력해 주세요.';
    if (strlen($password) < 12 || strlen($password) > 72 || !preg_match('/[A-Za-z]/', $password) || !preg_match('/[0-9]/', $password)) {
        $errors[] = '비밀번호는 영문과 숫자를 포함해 12자 이상 입력해 주세요.';
    }
    if (!hash_equals($password, $passwordConfirmation)) $errors[] = '비밀번호 확인이 일치하지 않습니다.';

    if ($errors) {
        http_response_code(422);
        render('admin/setup', compact('username', 'displayName', 'errors') + ['title'=>'관리자 계정 등록'], 'layouts/admin');
        exit;
    }

    try {
        $adminRepository = new AdminRepository();
        if ($adminRepository->adminCount() > 0) redirect('/admin/login');
        $adminRepository->createAdmin($username, $displayName, $password);
        $tokenFile = (string) (getenv('GTSIT_ADMIN_SETUP_TOKEN_FILE') ?: '');
        if ($tokenFile !== '' && is_file($tokenFile)) unlink($tokenFile);
        unset($_SESSION['admin_setup_verified']);
        $_SESSION['admin_created'] = true;
        redirect('/admin/login');
    } catch (Throwable) {
        http_response_code(503);
        render('admin/setup', [
            'title'=>'관리자 계정 등록','username'=>$username,'displayName'=>$displayName,
            'errors'=>['계정을 생성하지 못했습니다. 잠시 후 다시 시도해 주세요.'],
        ], 'layouts/admin');
        exit;
    }
}

if ($path === '/admin/login' && $method === 'GET') {
    if (AdminAuth::check()) redirect('/admin');
    $notice = !empty($_SESSION['admin_created']) ? '관리자 계정이 생성되었습니다. 설정한 비밀번호로 로그인해 주세요.' : '';
    unset($_SESSION['admin_created']);
    render('admin/login', ['title'=>'관리자 로그인','error'=>'','notice'=>$notice,'username'=>'gts0201'], 'layouts/admin');
    exit;
}

if ($path === '/admin/login' && $method === 'POST') {
    verify_csrf();
    $username = trim((string) ($_POST['username'] ?? ''));
    $password = (string) ($_POST['password'] ?? '');
    $loginRate = $_SESSION['admin_login_rate'] ?? ['started'=>time(),'attempts'=>0];
    if (time() - (int)$loginRate['started'] > 900) $loginRate = ['started'=>time(),'attempts'=>0];
    if ((int)$loginRate['attempts'] >= 10) {
        http_response_code(429);
        render('admin/login', ['title'=>'관리자 로그인','error'=>'로그인 시도가 많습니다. 잠시 후 다시 시도해 주세요.','notice'=>'','username'=>$username], 'layouts/admin');
        exit;
    }
    $authenticated = false;
    try { $authenticated = (new AdminAuth())->attempt($username, $password); } catch (Throwable) {}
    if ($authenticated) {
        unset($_SESSION['admin_login_rate']);
        redirect('/admin');
    }
    $loginRate['attempts']++;
    $_SESSION['admin_login_rate'] = $loginRate;
    http_response_code(422);
    render('admin/login', ['title'=>'관리자 로그인','error'=>'아이디 또는 비밀번호를 확인해 주세요.','notice'=>'','username'=>$username], 'layouts/admin');
    exit;
}

if ($path === '/admin/logout' && $method === 'POST') {
    verify_csrf();
    AdminAuth::logout();
    redirect('/admin/login');
}

if (str_starts_with($path, '/admin')) {
    AdminAuth::requireLogin();
    try { $adminRepository = new AdminRepository(); } catch (Throwable) {
        http_response_code(503);
        render('errors/404', ['title'=>'관리 데이터를 불러올 수 없습니다'], 'layouts/admin');
        exit;
    }

    if ($path === '/admin' && $method === 'GET') {
        render('admin/dashboard', ['title'=>'대시보드','counts'=>$adminRepository->dashboardCounts()], 'layouts/admin');
        exit;
    }
    if ($path === '/admin/posts' && $method === 'GET') {
        render('admin/posts/index', ['title'=>'글 관리','posts'=>$adminRepository->posts()], 'layouts/admin');
        exit;
    }
    if ($path === '/admin/posts/new' && $method === 'GET') {
        render('admin/posts/form', ['title'=>'새 글 작성','post'=>null,'errors'=>[]], 'layouts/admin');
        exit;
    }
    if ($path === '/admin/posts/new' && $method === 'POST') {
        verify_csrf();
        $postData = collect_post_input();
        $errors = validate_post_input($postData, $adminRepository);
        [$image, $imageError] = prepare_post_image($_FILES['image'] ?? null);
        if ($imageError !== null) $errors[] = $imageError;
        if ($image !== null && $postData['image_alt'] === '') $errors[] = '대표 이미지의 대체 텍스트를 입력해 주세요.';
        if ($errors) {
            http_response_code(422);
            render('admin/posts/form', ['title'=>'새 글 작성','post'=>null,'errors'=>$errors], 'layouts/admin');
            exit;
        }
        $newImagePath = null;
        try {
            if ($image !== null) $newImagePath = store_post_image($image);
            $adminRepository->savePostWithImage($postData, null, $newImagePath, $postData['image_alt']);
        } catch (Throwable) {
            if ($newImagePath !== null) remove_stored_image($newImagePath);
            http_response_code(503);
            render('admin/posts/form', ['title'=>'새 글 작성','post'=>null,'errors'=>['저장하지 못했습니다. 잠시 후 다시 시도해 주세요.']], 'layouts/admin');
            exit;
        }
        redirect('/admin/posts');
    }
    if (preg_match('#^/admin/posts/(\d+)/edit$#', $path, $adminPostMatch) === 1) {
        $postId = (int) $adminPostMatch[1];
        $post = $adminRepository->findPost($postId);
        if (!$post) {
            http_response_code(404);
            render('errors/404', ['title'=>'게시글을 찾을 수 없습니다'], 'layouts/admin');
            exit;
        }
        if ($method === 'GET') {
            render('admin/posts/form', ['title'=>'글 수정','post'=>$post,'errors'=>[]], 'layouts/admin');
            exit;
        }
        if ($method === 'POST') {
            verify_csrf();
            $postData = collect_post_input();
            $errors = validate_post_input($postData, $adminRepository, $postId);
            [$image, $imageError] = prepare_post_image($_FILES['image'] ?? null);
            if ($imageError !== null) $errors[] = $imageError;
            if (($image !== null || !empty($post['image_path'])) && $postData['image_alt'] === '') $errors[] = '대표 이미지의 대체 텍스트를 입력해 주세요.';
            if ($errors) {
                http_response_code(422);
                render('admin/posts/form', ['title'=>'글 수정','post'=>$post,'errors'=>$errors], 'layouts/admin');
                exit;
            }
            $newImagePath = null;
            try {
                if ($image !== null) $newImagePath = store_post_image($image);
                $result = $adminRepository->savePostWithImage($postData, $postId, $newImagePath, $postData['image_alt']);
                foreach ($result['old_paths'] as $oldPath) remove_stored_image((string) $oldPath);
            } catch (Throwable) {
                if ($newImagePath !== null) remove_stored_image($newImagePath);
                http_response_code(503);
                render('admin/posts/form', ['title'=>'글 수정','post'=>$post,'errors'=>['저장하지 못했습니다. 잠시 후 다시 시도해 주세요.']], 'layouts/admin');
                exit;
            }
            redirect('/admin/posts');
        }
    }
    if (preg_match('#^/admin/posts/(\d+)/delete$#', $path, $adminDeleteMatch) === 1 && $method === 'POST') {
        verify_csrf();
        $adminRepository->deletePost((int) $adminDeleteMatch[1]);
        redirect('/admin/posts');
    }
    if ($path === '/admin/inquiries' && $method === 'GET') {
        render('admin/inquiries/index', ['title'=>'문의 관리','inquiries'=>$adminRepository->inquiries()], 'layouts/admin');
        exit;
    }
    if (preg_match('#^/admin/inquiries/(\d+)$#', $path, $adminInquiryMatch) === 1) {
        $inquiryId = (int) $adminInquiryMatch[1];
        $inquiry = $adminRepository->findInquiry($inquiryId);
        if (!$inquiry) {
            http_response_code(404);
            render('errors/404', ['title'=>'문의를 찾을 수 없습니다'], 'layouts/admin');
            exit;
        }
        if ($method === 'GET') {
            render('admin/inquiries/show', ['title'=>'문의 상세','inquiry'=>$inquiry], 'layouts/admin');
            exit;
        }
        if ($method === 'POST') {
            verify_csrf();
            $allowedStatuses = ['pending','in_progress','replied','closed'];
            $status = (string) ($_POST['status'] ?? 'pending');
            $reply = trim((string) ($_POST['reply'] ?? ''));
            if (!in_array($status, $allowedStatuses, true) || mb_strlen($reply) > 5000) {
                http_response_code(422);
                render('admin/inquiries/show', ['title'=>'문의 상세','inquiry'=>$inquiry], 'layouts/admin');
                exit;
            }
            if ($reply !== '' && $status === 'pending') $status = 'replied';
            $adminRepository->updateInquiry($inquiryId, $status, $reply);
            redirect('/admin/inquiries/' . $inquiryId);
        }
    }
    if ($path === '/admin/company') {
        $defaults = [
            'legal_name'=>(string)config('app.legal_name'),'ceo'=>'서선배',
            'phone'=>(string)config('app.contact.phone'),'phone_sub'=>(string)config('app.contact.phone_sub'),
            'fax'=>(string)config('app.contact.fax'),'email'=>(string)config('app.contact.email'),
            'address'=>(string)config('app.contact.address'),'bizno'=>'206-86-70582',
            'business_hours'=>(string)config('app.contact.business_hours'),
        ];
        $settingRepository = new SiteSettingRepository();
        $settings = array_merge($defaults, $settingRepository->all());
        if ($method === 'GET') {
            render('admin/company', ['title'=>'회사 정보','settings'=>$settings,'errors'=>[]], 'layouts/admin');
            exit;
        }
        if ($method === 'POST') {
            verify_csrf();
            $submitted = [];
            foreach (array_keys($defaults) as $key) $submitted[$key] = trim((string)($_POST[$key]??''));
            $errors = [];
            if ($submitted['legal_name']==='' || mb_strlen($submitted['legal_name'])>120) $errors[]='법인명을 확인해 주세요.';
            if (!preg_match('/^[0-9+() -]{8,30}$/',$submitted['phone'])) $errors[]='대표번호를 확인해 주세요.';
            if ($submitted['email']!=='' && filter_var($submitted['email'],FILTER_VALIDATE_EMAIL)===false) $errors[]='이메일 형식을 확인해 주세요.';
            if (mb_strlen($submitted['address'])>255) $errors[]='주소를 255자 이내로 입력해 주세요.';
            if ($errors) {
                http_response_code(422);
                render('admin/company', ['title'=>'회사 정보','settings'=>$settings,'errors'=>$errors], 'layouts/admin');
                exit;
            }
            $settingRepository->save($submitted);
            redirect('/admin/company');
        }
    }

    http_response_code(404);
    render('errors/404', ['title'=>'관리자 페이지를 찾을 수 없습니다'], 'layouts/admin');
    exit;
}

http_response_code(404);
render('errors/404', ['title' => '페이지를 찾을 수 없습니다']);

function collect_post_input(): array
{
    return [
        'title'=>trim((string)($_POST['title']??'')),
        'slug'=>trim((string)($_POST['slug']??'')),
        'category'=>trim((string)($_POST['category']??'')),
        'excerpt'=>trim((string)($_POST['excerpt']??'')),
        'body'=>trim((string)($_POST['body']??'')),
        'author'=>trim((string)($_POST['author']??'')),
        'status'=>(string)($_POST['status']??'draft'),
        'published_at'=>'',
        'image_alt'=>trim((string)($_POST['image_alt']??'')),
    ];
}

function validate_post_input(array $data, AdminRepository $repository, ?int $exceptId = null): array
{
    $errors = [];
    if ($data['title']==='' || mb_strlen($data['title'])>200) $errors[]='제목을 200자 이내로 입력해 주세요.';
    if (!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $data['slug'])) $errors[]='slug는 영문 소문자, 숫자, 하이픈으로 입력해 주세요.';
    elseif ($repository->slugExists($data['slug'], $exceptId)) $errors[]='이미 사용 중인 slug입니다.';
    if (!in_array($data['category'], ['네트워크','키폰시스템','CCTV','시공사례'], true)) $errors[]='카테고리를 확인해 주세요.';
    if (mb_strlen($data['excerpt'])>500) $errors[]='목록 요약을 500자 이내로 입력해 주세요.';
    if ($data['body']==='') $errors[]='본문을 입력해 주세요.';
    if ($data['author']==='' || mb_strlen($data['author'])>100) $errors[]='작성자를 확인해 주세요.';
    if (!in_array($data['status'], ['draft','published'], true)) $errors[]='공개 상태를 확인해 주세요.';
    if (mb_strlen($data['image_alt'])>255) $errors[]='이미지 대체 텍스트를 255자 이내로 입력해 주세요.';
    return $errors;
}

function prepare_post_image(?array $file): array
{
    if ($file === null || ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return [null, null];
    if (!isset($file['error'], $file['size'], $file['tmp_name']) || is_array($file['error']) || is_array($file['size']) || is_array($file['tmp_name'])) {
        return [null, '이미지 업로드 요청이 올바르지 않습니다.'];
    }
    if ((int) $file['error'] !== UPLOAD_ERR_OK) return [null, '이미지를 업로드하지 못했습니다. 파일 크기를 확인해 주세요.'];
    if ((int) $file['size'] <= 0 || (int) $file['size'] > 8 * 1024 * 1024) return [null, '대표 이미지는 8MB 이하만 등록할 수 있습니다.'];

    $tmpName = (string) $file['tmp_name'];
    if (!is_uploaded_file($tmpName)) return [null, '업로드된 이미지 파일을 확인할 수 없습니다.'];
    $mime = (new finfo(FILEINFO_MIME_TYPE))->file($tmpName);
    $extensions = ['image/jpeg'=>'jpg', 'image/png'=>'png', 'image/webp'=>'webp'];
    if (!is_string($mime) || !isset($extensions[$mime])) return [null, 'JPG, PNG, WebP 이미지만 등록할 수 있습니다.'];
    $dimensions = getimagesize($tmpName);
    if ($dimensions === false || $dimensions[0] < 1 || $dimensions[1] < 1 || $dimensions[0] > 6000 || $dimensions[1] > 6000) {
        return [null, '이미지 크기는 최대 6000×6000px까지 등록할 수 있습니다.'];
    }
    if (($dimensions['mime'] ?? '') !== $mime) return [null, '이미지 파일의 형식이 올바르지 않습니다.'];

    return [['tmp_name'=>$tmpName, 'extension'=>$extensions[$mime]], null];
}

function store_post_image(array $image): string
{
    $relativeDirectory = '/uploads/posts/' . date('Y/m');
    $publicRoot = rtrim((string) config('app.public_root', __DIR__), '/');
    $absoluteDirectory = $publicRoot . $relativeDirectory;
    if (!is_dir($absoluteDirectory) && !mkdir($absoluteDirectory, 0750, true) && !is_dir($absoluteDirectory)) {
        throw new RuntimeException('Upload directory could not be created.');
    }
    $relativePath = $relativeDirectory . '/' . bin2hex(random_bytes(20)) . '.' . $image['extension'];
    if (!move_uploaded_file($image['tmp_name'], $publicRoot . $relativePath)) {
        throw new RuntimeException('Uploaded file could not be moved.');
    }
    chmod($publicRoot . $relativePath, 0640);
    return $relativePath;
}

function remove_stored_image(string $path): void
{
    if (!str_starts_with($path, '/uploads/posts/')) return;
    $absolutePath = rtrim((string) config('app.public_root', __DIR__), '/') . $path;
    if (is_file($absolutePath)) unlink($absolutePath);
}
