<?php

namespace Acms\Services\Update\System;

use Acms\Services\Update\Contracts\LoggerInterface;
use Acms\Services\Update\Exceptions\IncompleteDownloadException;
use Acms\Services\Facades\LocalStorage;
use Acms\Services\Facades\Http;

class Download
{
    /**
     * @var \Acms\Services\Update\Contracts\LoggerInterface
     */
    protected $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @param string $dest_path
     * @param string $url
     */
    public function download($dest_path, $url)
    {
        $this->logger->message(gettext('パッケージをダウンロード中...') . ' (' . $url . ')', 5);

        set_time_limit(0);
        LocalStorage::makeDirectory($dest_path);

        $filename = basename($url);
        $path = $dest_path . $filename;
        $fp = fopen($path, 'w+');
        if ($fp === false) {
            throw new \RuntimeException(sprintf(gettext('パッケージを保存するためのファイルを開けませんでした: %s'), $path));
        }

        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_FILE, $fp);
        curl_setopt($curl, CURLOPT_TIMEOUT, 0);

        Http::setCurlSsl($curl);
        Http::setCurlProxy($curl);

        $succeeded = curl_exec($curl);
        $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        // Why not CURLINFO_CONTENT_LENGTH_DOWNLOAD_T 直書き: リンクされた libcurl が 7.55.0 未満のビルドでは
        // 定数自体が未定義で Undefined constant になる（PHP 拡張のバージョンでなく libcurl 側のビルド依存）。
        $contentLengthInfo = defined('CURLINFO_CONTENT_LENGTH_DOWNLOAD_T')
            ? CURLINFO_CONTENT_LENGTH_DOWNLOAD_T
            : CURLINFO_CONTENT_LENGTH_DOWNLOAD;
        $contentLength = (int)curl_getinfo($curl, $contentLengthInfo);
        $error = curl_error($curl);
        // getFileSize() で実際に保存されたサイズを見るため、書き込みバッファを確定させる
        fclose($fp);

        if ($succeeded === false || $status !== 200) {
            throw new \RuntimeException(sprintf(gettext('パッケージのダウンロードに失敗しました。%s: %s'), (string)$status, $error));
        }
        $this->validateDownloadedSize($path, $contentLength);

        $this->logger->message(gettext('パッケージダウンロード完了'), 15);
        $this->logger->message(gettext('パッケージを解凍中...'), 0);

        LocalStorage::unzip($path, $dest_path);
        LocalStorage::remove($path);

        $this->logger->message(gettext('パッケージを解凍完了'), 30);
    }

    /**
     * ダウンロードしたパッケージのサイズが Content-Length と一致するか検証する
     *
     * ダウンロードが途中で切れても HTTP ステータスは 200 のままなので、
     * 不完全なファイルを解凍しようとして原因の分かりにくい失敗になるのを防ぐ。
     *
     * Why public: download() は curl 経由の実通信に依存し直接テストしづらいため、
     * 副作用を持たないこの検証ロジックだけを切り出して単体テスト可能にしている。
     *
     * @param string $path
     * @param int $expectedSize Content-Length。取得できなかった場合は 0
     * @throws IncompleteDownloadException
     * @return void
     */
    public function validateDownloadedSize(string $path, int $expectedSize): void
    {
        if ($expectedSize <= 0) {
            return;
        }
        $actualSize = LocalStorage::getFileSize($path);
        if ($actualSize === $expectedSize) {
            return;
        }
        throw new IncompleteDownloadException(sprintf(
            gettext('ダウンロードしたパッケージが不完全です。期待サイズ: %d バイト, 実際のサイズ: %d バイト'),
            $expectedSize,
            $actualSize
        ));
    }
}
