<?php

namespace platyFramework;

require_once __DIR__ . '/../cli/ptyCliLog.php';

/**
 * ptyCrawling - law.go.kr 웹 크롤링 클래스
 *
 * law.go.kr API에서 법률 데이터를 크롤링하고
 * 응답을 data 디렉토리에 파일로 저장합니다.
 */
class ptyCrawling
{
    private $dataDir = 'data';
    private $lastFetchWasFromCache = false;

    /** @var ptyCliLog */
    private $log;

    /**
     * 생성자
     * data 디렉토리가 없으면 생성합니다.
     */
    public function __construct($dataDir = 'data', ptyCliLog $log = null)
    {
        $this->dataDir = $dataDir;
        if (!is_dir($this->dataDir)) {
            mkdir($this->dataDir, 0755, true);
        }

        $this->log = $log ?? ptyCliLog();
    }

    /**
     * API에서 페이지별 항목 가져오기
     *
     * @param string $url {page} 플레이스홀더가 포함된 URL 패턴
     * @param callable|null $isContinue 크롤링 계속 여부를 결정하는 콜백 함수
     *                                  응답 본문을 매개변수로 받음
     *                                  계속하려면 true, 중지하려면 false 반환
     * @param int $reloadCacheTime 캐시 유효 시간(초). 0이면 캐시 사용 안 함
     *                             예: 60*60 = 3600이면 1시간 이내 캐시 사용
     * @return array 크롤링된 데이터 정보 배열
     */
    public function getPageItems($url, $isContinue = null, $reloadCacheTime = 60 * 60 * 24 * 30)
    {
        $page = 1;
        $crawledFiles = [];
        $cacheCrawledFiles = [];
        $saveCrawledFiles = [];

        while (true) {
            // {page} 플레이스홀더를 현재 페이지 번호로 치환
            $currentUrl = str_replace('{page}', $page, $url);

            $this->log->info("");
            $this->log->info("페이지 $page - 처리중: $currentUrl");

            // getAndSave 메서드를 사용하여 데이터 가져오기 및 저장
            $body = $this->_getUrlContents($currentUrl, $reloadCacheTime);

            if ($body === false) {
                $this->log->error("Page $page - Failed to fetch URL");
                break;
            }

            // 파일명과 캐시 정보 추적
            $filename = $this->dataDir . '/' . $this->_urlToFilename($currentUrl);
            $wasFromCache = $this->lastFetchWasFromCache;

            if ($wasFromCache) {
                $cacheCrawledFiles[] = $filename;
            } else {
                $saveCrawledFiles[] = $filename;
            }
            $crawledFiles[] = $filename;

            $shouldContinue = $this->onCrawlingPaging($page, $body);
            if (!$shouldContinue) {
                $this->log->info("Page $page - Callback returned false. Stopping.");
                break;
            }

            /*
            // isContinue 콜백이 제공된 경우 호출
            if ($isContinue !== null && is_callable($isContinue)) {
                $shouldContinue = call_user_func($isContinue, $page, $body);
                if (!$shouldContinue) {
                    $this->log->info("Page $page - Callback returned false. Stopping.");
                    break;
                }
            }
            */

            // 다음 페이지로 이동
            $page++;
        }

        $this->log->success("Cache pages: " . count($cacheCrawledFiles));
        $this->log->success("New pages saved: " . count($saveCrawledFiles));
        $this->log->success("Total pages crawled: " . count($crawledFiles));

        return $crawledFiles;
    }


    /**
     * 콘텐츠를 파일로 저장
     *
     * @param string $url 원본 URL
     * @param string $content 저장할 콘텐츠
     * @return string|false 성공 시 파일명, 실패 시 false
     */
    private function _saveToFile($url, $content)
    {
        // URL을 안전한 파일명으로 변환
        $filename = $this->_urlToFilename($url);
        $filepath = $this->dataDir . '/' . $filename;

        // 필요한 경우 하위 디렉토리 생성
        $dir = dirname($filepath);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }

        // 파일 저장
        $result = file_put_contents($filepath, $content);

        return $result !== false ? $filepath : false;
    }

    /**
     * URL을 안전한 파일명으로 변환
     * 특수 문자를 치환하고 읽기 가능한 파일명을 생성합니다.
     *
     * @param string $url 변환할 URL
     * @return string 안전한 파일명
     */
    private function _urlToFilename($url)
    {
        // 프로토콜 제거
        $filename = preg_replace('#^https?://#', '', $url);

        // 특수 문자를 언더스코어로 치환
        $filename = preg_replace('#[^a-zA-Z0-9._-]#', '_', $filename);

        // 연속된 언더스코어 제거
        $filename = preg_replace('#_+#', '_', $filename);

        // .json 확장자가 없으면 추가
        if (!preg_match('#\.body#i', $filename)) {
            $filename .= '.body';
        }

        return $filename;
    }

    /**
     * 커스텀 데이터 디렉토리 설정
     *
     * @param string $dir 디렉토리 경로
     */
    public function _setDataDir($dir)
    {
        $this->dataDir = $dir;
        if (!is_dir($this->dataDir)) {
            mkdir($this->dataDir, 0755, true);
        }
    }

    public function getUrlFileName($url) {
        $cacheFilename = $this->_urlToFilename($url);
        $cacheFilepath = $this->dataDir . '/' . $cacheFilename;

        return $cacheFilepath;
    }

    /**
     * URL에서 데이터를 가져오고 파일로 저장
     * 캐시 확인, fetch, 저장을 하나의 메서드에서 처리
     *
     * @param string $url 가져올 URL
     * @param int $reloadCacheTime 캐시 유효 시간(초). 0이면 캐시 사용 안 함
     * @return string|false 성공 시 응답 본문, 실패 시 false
     */
    public function _getUrlContents($url, $reloadCacheTime = 60 * 60 * 24 * 30)
    {
        $this->log->url->verbose("URL 가져오는중: $url");
        // 캐시 파일 경로 생성
        $cacheFilename = $this->_urlToFilename($url);
        $cacheFilepath = $this->dataDir . '/' . $cacheFilename;

        if (file_exists($cacheFilepath)) ;
        else if (file_exists($cacheFilepath . ".in_progress")) $cacheFilepath = $cacheFilepath . ".in_progress";
        else if (file_exists($cacheFilepath . ".success")) $cacheFilepath = $cacheFilepath . ".success";

        // 캐시 사용이 활성화된 경우, 캐시 확인
        if ($reloadCacheTime > 0 && file_exists($cacheFilepath)) {
            $fileModTime = filemtime($cacheFilepath);
            $currentTime = time();
            $fileAge = $currentTime - $fileModTime;

            // 캐시가 유효 시간 이내인 경우
            if ($fileAge < $reloadCacheTime) {
                $ageMinutes = round($fileAge / 60, 1);
                $this->log->url->verbose("캐시 가져옴: " . $cacheFilepath);
                // $this->log->verbose("Cache hit (age: {$ageMinutes}m): " . basename($cacheFilepath));
                $this->lastFetchWasFromCache = true;
                return file_get_contents($cacheFilepath);
            } else {
                $ageMinutes = round($fileAge / 60, 1);
                $maxMinutes = round($reloadCacheTime / 60, 1);
                // $this->log->verbose("캐시 만료됨: " . $filename);
                $this->log->url->verbose("Cache expired (age: {$ageMinutes}m, max: {$maxMinutes}m, 파일명: $cacheFilepath)");
            }
        }

        // 캐시가 없거나 만료된 경우, 새로 fetch
        $this->lastFetchWasFromCache = false;

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; ptyCrawler/1.0)');

        $body = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            $this->log->url->error("HTTP Error: $httpCode");
            return false;
        }

        if ($body === false) {
            $this->log->url->error("Failed to fetch URL");
            return false;
        }

        // 파일로 저장
        $filename = $this->_saveToFile($url, $body);
        if (!$filename) {
            $this->log->url->error("Failed to save file");
            return false;
        }

        // $this->log->verbose("저장됨: " . basename($filename));
        $this->log->url->verbose("저장됨: " . $filename);

        // 서버 부담을 줄이기 위한 짧은 지연
        // usleep(500000); // 0.5초 지연

        return $body;
    }

    public function onCrawlingPaging($pageIndex, $body)
    {
        return true;
    }
}
