<?php

class QAHM_Subcron_Proc extends QAHM_File_Data {
	
	public function __construct() {

		$this->init_wp_filesystem();

	}

	private function url_to_baseurl($url) {
		$parsed_url = parse_url($url);
	
		$base_url = $parsed_url['scheme'] . '://' . $parsed_url['host'];
	
		if (isset($parsed_url['path'])) {
			$path = pathinfo($parsed_url['path'], PATHINFO_DIRNAME);
			// pathinfoはディレクトリのパスがルートの場合に'/'を返さないため、
			// ルートの場合は手動で'/'を追加する
			if ($path === '.') {
				$path = '/';
			} elseif (substr($path, -1) !== '/') {
				// パスが'/'で終わっていない場合は'/'を追加する
				$path .= '/';
			}
			$base_url .= $path;
		}
	
		if (isset($parsed_url['port'])) {
			$base_url .= ':' . $parsed_url['port'];
		}
	
		return $base_url;
	}

	private function remove_query_from_url( $url ) {

		$parsed_url = parse_url($url);
		$scheme     = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
		$host       = isset($parsed_url['host']) ? $parsed_url['host'] : '';
		$port       = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
		$path       = isset($parsed_url['path']) ? $parsed_url['path'] : '';
		
		return "$scheme$host$port$path";

	}

	private function remove_query_params_from_body($dom) {
		$xpath = new DOMXPath($dom);
		// <body>内の<script>と<link>要素を対象にXPathクエリを実行
		$scriptNodes = $xpath->query('//body//script[@src]');
		$linkNodes = $xpath->query('//body//link[@rel="stylesheet"][@href]');
	
		// <script>タグのsrc属性からクエリパラメータを除去
		foreach ($scriptNodes as $node) {
			$src = $node->getAttribute('src');
			$node->setAttribute('src', $this->remove_query_from_url($src));
		}
	
		// <link>タグのhref属性からクエリパラメータを除去
		foreach ($linkNodes as $node) {
			$href = $node->getAttribute('href');
			$node->setAttribute('href', $this->remove_query_from_url($href));
		}
	}

	private function release_dom_resources(&$old_dom, &$new_dom) {
		unset($old_dom, $new_dom);
		gc_collect_cycles();
	}
	
	public function has_page_changed($old_html, $new_html, $base_url) {

		global $qahm_log;

		$base_url = $this->url_to_baseurl($base_url);
		$reason   = null;

		$res = apply_filters('qazero_pre_page_update_check', null, $old_html, $new_html, $base_url);
		if ( is_bool($res) ) {
			return $res;
		}

		if (strip_tags($old_html) === $old_html || strip_tags($new_html) === $new_html) {
			$reason = 99;
			return apply_filters('qazero_post_after_page_update_check', false, $old_html, $new_html, $base_url, $reason);
		}

		// DOMDocumentを使用してHTMLを解析
		$old_dom = new DOMDocument();
		$new_dom = new DOMDocument();

		// HTMLの読み込みに失敗した場合はfalseを返す
		if (!@$old_dom->loadHTML($old_html) || !@$new_dom->loadHTML($new_html)) {
			$reason = 99;
			$this->release_dom_resources($old_dom, $new_dom);
			return apply_filters('qazero_post_after_page_update_check', false, $old_html, $new_html, $base_url, $reason);
		}

		$this->remove_query_params_from_body($old_dom);
		$this->remove_query_params_from_body($new_dom);

		// <body>の中身の変更を確認
		$old_body = $old_dom->getElementsByTagName('body')->item(0);
		$new_body = $new_dom->getElementsByTagName('body')->item(0);

		// body要素が存在しない場合はfalseを返す
		if (!$old_body || !$new_body) {
			$reason = 99;
			$this->release_dom_resources($old_dom, $new_dom);
			return apply_filters('qazero_post_after_page_update_check', false, $old_html, $new_html, $base_url, $reason);
		}

		if ($old_body->C14N() !== $new_body->C14N()) {
			$reason = 1;
			$res = apply_filters('qazero_post_after_page_update_check', true, $old_html, $new_html, $base_url, $reason);
			if ( $res === true ) {
				$this->release_dom_resources($old_dom, $new_dom);
				return true;
			}
		}
	
		// <head>の中で読み込む外部javascriptファイルの増減を確認
		$old_scripts = $old_dom->getElementsByTagName('script');
		$new_scripts = $new_dom->getElementsByTagName('script');
		if ($old_scripts->length !== $new_scripts->length) {
			$reason = 2;
			$res = apply_filters('qazero_post_after_page_update_check', true, $old_html, $new_html, $base_url, $reason);
			if ( $res === true ) {
				$this->release_dom_resources($old_dom, $new_dom);
				return true;
			}
		}

		$reason = 0;
		$res = apply_filters('qazero_post_after_page_update_check', false, $old_html, $new_html, $base_url, $reason);
		$this->release_dom_resources($old_dom, $new_dom);

		return $res;
	}

	private function resolve_url($base, $url) {
		// $urlが既に絶対パスの場合はそのまま返す
		if (parse_url($url, PHP_URL_SCHEME) != '') return $url;
		// 相対パスを絶対パスに変換
		return rtrim($base, '/') . '/' . ltrim($url, '/');
	}


	function remove_excluded_params_from_url($url, $exclude_params) {
		if ( empty($exclude_params) ) {
			return $url;
		}

		// URLを分解してクエリ部分を取得
		$parsed_url = parse_url($url);
		parse_str($parsed_url['query'] ?? '', $query_params);
	
		// クエリパラメータから除外するパラメータを削除
		foreach ($exclude_params as $param) {
			unset($query_params[$param]);
		}
	
		// 残ったパラメータをクエリ文字列に変換
		$new_query = http_build_query($query_params);
	
		// 新しいURLを組み立てる
		$new_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] .
				   (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '') .
				   $parsed_url['path'] .
				   (!empty($new_query) ? '?' . $new_query : '');
	
		return $new_url;
	}


	/*
		$subcron_listの構造

		urlの下に多数のデータを構築しているのは、次ステップでキー配列として高速にアクセスするため
		なおサイトマップ関連のコードは廃止予定なので対応しない

		Array
		(
			[url] => Array
				(
					[url] => ''
					[tracking_id] => tracking_id
					[pv] => 0
					[page_ids] => Array
						(
							[page_id_1] => Array
								(
									[device_ids] => Array
										(
											[device_id_1] => version_id
											[device_id_2] => version_id
											[device_id_3] => version_id
										)

								)

							[page_id_2] => Array
								(
									[device_ids] => Array
										(
											[device_id_1] => version_id
											[device_id_2] => version_id
											[device_id_3] => version_id
										)

								)
						)

				)

			...
		)

		除外URLパラメータ、検索パラメータの構造
		Array
		(
			[tracking_id] => Array
				(
					[0] => value1
					[1] => value2
					...
				)

		)
		...
	*/
	private function create_subcron_list() {
		global $wpdb;
		global $qahm_log;
		global $qahm_data_api;
	
		$pages_table        = $wpdb->prefix . 'qa_pages';
		$version_hist_table = $wpdb->prefix . 'qa_page_version_hist';

		$sitemanage         = $qahm_data_api->get_sitemanage();
		$sitemanage = is_array($sitemanage)
		? array_values(array_filter($sitemanage, function($item) {
			return $item['get_base_html_periodic'] === 1;
		}))
		: [];
		if ( empty( $sitemanage ) ){
			echo 'sitemanageにて、取得対象となるサイトが存在しません。';
			$qahm_log->warning( 'sitemanageにて、取得対象となるサイトが存在しません。', 'qalog_subcron.txt' );
			return false;
		}

		// device_idの配列を取得
		$device_ids = array_column(QAHM_DEVICES, 'id');
		$device_ids_list = implode(', ', $device_ids); // カンマ区切りのリストを作成

		// タグ発行 > HTML自動取得が「する」になっているtracking_idの配列を作成
		$tracking_ids = array_column($sitemanage, 'tracking_id');
		$tracking_ids_list = "'" . implode("', '", $tracking_ids) . "'"; // シングルクォートで囲んでカンマ区切りリストを作成

		// tracking_idをキーにした$ignore_params, $search_paramsの配列を作成
		$search_params_option = $this->wrap_get_option('search_params');
		$search_params = $search_params_option ? json_decode($search_params_option, true) : null;
		$ignore_params = null;
		foreach ($sitemanage as $row) {
			$tracking_id = $row['tracking_id'];

			if ( $row['ignore_params'] ) {
				// ignore_params
				// カンマ区切りのignore_paramsを配列に変換
				$ignore_params_array = explode(',', $row['ignore_params']);
				
				// 配列内の要素をトリムして不要な空白を削除
				$ignore_params_array = array_map('trim', $ignore_params_array);

				// tracking_idをキーとしてignore_paramsの配列を格納
				$ignore_params[$tracking_id] = $ignore_params_array;
			}

			// search_params
			if ( $search_params ) {
				// カンマ区切りのsearch_paramsを配列に変換
				$search_params[$tracking_id] = explode(',', $search_params[$tracking_id]);
				
				// 配列内の要素をトリムして不要な空白を削除
				$search_params[$tracking_id] = array_map('trim', $search_params[$tracking_id]);
			}
		}

		// JOINを使ったクエリで、page_idに対する各device_idの最新version_idを取得
		$query = "
			SELECT p.page_id, p.url, p.tracking_id, vh.device_id, MAX(vh.version_id) as version_id
			FROM $pages_table p
			LEFT JOIN $version_hist_table vh ON p.page_id = vh.page_id
			WHERE vh.device_id IN ($device_ids_list)
				AND p.tracking_id IN ($tracking_ids_list)
			GROUP BY p.page_id, p.url, vh.device_id
			ORDER BY vh.device_id
		";
		$results = $wpdb->get_results($query, ARRAY_A);
	
		if (empty($results)) {
			echo 'qa_pagesに対象データが存在しません。';
			$qahm_log->error( 'qa_pagesに対象データが存在しません。', 'qalog_subcron.txt' );
			return false;
		}
	
		$subcron_list = [];
	
		// 一部のURLは除外して$subcron_listを構築
		foreach ($results as $row) {
			$tracking_id = $row['tracking_id'];
			$page_id     = $row['page_id'];
			$device_id   = $row['device_id'];
			$clean_url   = $this->url_cleansing($row['url']);
			$clean_url   = $this->remove_excluded_params_from_url($clean_url, $ignore_params[$tracking_id]);
			$clean_url   = $this->remove_excluded_params_from_url($clean_url, $search_params[$tracking_id]);

			// URLをキーとして使用し、エントリがない場合は初期化
			if (!isset($subcron_list[$clean_url])) {
				$subcron_list[$clean_url] = [
					'url' => '',
					'tracking_id' => $tracking_id,
					'pv' => 0,
					'page_ids' => []
				];
			}

			// page_idの配列を初期化
			if (!isset($subcron_list[$clean_url]['page_ids'][$page_id])) {
				$subcron_list[$clean_url]['page_ids'][$page_id] = [
					'device_ids' => []
				];
			}

			// device_idとversion_idを格納
			$subcron_list[$clean_url]['page_ids'][$page_id]['device_ids'][$device_id] = $row['version_id'];
		}

		$this->wrap_put_contents($this->get_data_dir_path() . 'subcron_list.php', $this->wrap_serialize( $subcron_list) );
		$qahm_log->info( '対象URL数: ' . count($subcron_list), 'qalog_subcron.txt' );
	
		// 結果を確認（オプション）
		echo '<pre>';
		echo print_r($subcron_list, true);
		echo '</pre>';

		return true;
	}

	
	/*
		この処理が出来上がっている時点でsubcron_listが出来上がっているのでソートを行う
		ソートの順序としては、LPのデータを参照してPV数を設定後、多い順にソートする

		返り値がfalseならデータ破損とみなし、前のステップに戻る
		返り値がtrueなら次のステップに進む

		この処理ではソートだけではなく、subcron_listのキー値をインデックスに変換といったこともしているので
		関数名や出来上がるファイルにはorganizeとつけている
	*/
	public function organize_subcron_list() {
		global $qahm_log;
		global $qahm_time;
		global $qahm_data_api;
		global $wpdb;

		$data_dir           = $this->get_data_dir_path();
		$view_dir           = $data_dir . 'view/';
		$summary_dir        = $view_dir . 'all/summary/';
		$list_path          = $data_dir . 'subcron_list.php';
		$list_organize_path = $data_dir . 'subcron_list_organize.php';

		$list = $this->wrap_unserialize( $this->wrap_get_contents($list_path) );
		if( ! $list ){
			//データ破損とみなし、一旦削除
			$this->wrap_delete($list_path);
			echo $list_path . "を読み込めませんでした。<br>";
			$qahm_log->error( $list_path . "を読み込めませんでした。", 'qalog_subcron.txt' );
			return false;
		}

		// ファイル名パターンの指定
		$ap_fname_ptn = '/(\d{4}-\d{2}-\d{2})_summary_allpage\.php$/';

		// ファイルパターンに一致するファイルを取得
		$summary_files = glob($summary_dir . '/*_summary_allpage.php');

		// 日付を抽出して保存するための配列
		$matched_dates = [];

		foreach ($summary_files as $file) {
			if (preg_match($ap_fname_ptn, basename($file), $matches)) {
				$matched_dates[] = $matches[1]; // 日付部分を保存
			}
		}

		// 最新日付の取得と処理
		if (empty($matched_dates)) {
			// 例外処理: 昨日の日付を取得
			echo "summary file not found.<br>";
			$target_date = $qahm_time->xday_str(-1);
		} else {
			rsort($matched_dates); // 日付を降順にソート
			$target_date = $qahm_time->xday_str(0,$matched_dates[0]);
		}

		// 日付をフォーマットして変数に格納
		$date_term = "date = between " . $target_date . " and " . $target_date;

		// 結果の出力
		echo "最新の日付: $target_date<br>";
		echo "DateTerm: $date_term";

		$ap_ary = $qahm_data_api->get_ap_data( $date_term, 'all' );
		//echo count($ap_ary);

		if ( $ap_ary ) {
			// tracking_idをキーにした$ignore_params, $search_paramsの配列を作成
			$search_params_option = $this->wrap_get_option('search_params');
			$search_params = $search_params_option ? json_decode($search_params_option, true) : null;
			$ignore_params = null;

			$sitemanage    = $qahm_data_api->get_sitemanage();
			$sitemanage = is_array($sitemanage)
			? array_values(array_filter($sitemanage, function($item) {
				return $item['get_base_html_periodic'] === 1;
			}))
			: [];
			if ( empty( $sitemanage ) ){
				echo 'sitemanageにて、取得対象となるサイトが存在しません。';
				$qahm_log->warning( 'sitemanageにて、取得対象となるサイトが存在しません。', 'qalog_subcron.txt' );
				return false;
			}

			foreach ($sitemanage as $row) {
				$tracking_id = $row['tracking_id'];

				if ( $row['ignore_params'] ) {
					// ignore_params
					// カンマ区切りのignore_paramsを配列に変換
					$ignore_params_array = explode(',', $row['ignore_params']);

					// 配列内の要素をトリムして不要な空白を削除
					$ignore_params_array = array_map('trim', $ignore_params_array);

					// tracking_idをキーとしてignore_paramsの配列を格納
					$ignore_params[$tracking_id] = $ignore_params_array;
				}

				// search_params
				if ( $search_params ) {
					// カンマ区切りのsearch_paramsを配列に変換
					$search_params[$tracking_id] = explode(',', $search_params[$tracking_id]);

					// 配列内の要素をトリムして不要な空白を削除
					$search_params[$tracking_id] = array_map('trim', $search_params[$tracking_id]);
				}
			}

			foreach ( $ap_ary as $ap ) {
				$url = $ap[2];
				$pv  = (int) $ap[7];
				$clean_url = $this->url_cleansing($url);
				if ( isset( $ignore_params[$tracking_id] ) ) {
					$clean_url = $this->remove_excluded_params_from_url($clean_url, $ignore_params[$tracking_id]);
				}
				if ( isset( $search_params[$tracking_id] ) ) {
					$clean_url = $this->remove_excluded_params_from_url($clean_url, $search_params[$tracking_id]);
				}
				echo "URL: $clean_url, PV: $pv<br>";
				if( array_key_exists( $clean_url, $list ) ){
					$list[$clean_url]['pv'] += $pv;
				}
			}

			// pv順にソート
			uasort($list, function ($a, $b) {
				return $b['pv'] <=> $a['pv']; // 降順の場合
			});

		}

		// 次のステップ用にインデックス番号を付けた配列を作成
		$list_organize = [];
		foreach ($list as $url => $data) {
			// pvが0の場合は以降のデータはアクセス0なので無視
			if ( $data['pv'] === 0 ) {
				break;
			}
			$data['url'] = $url;
			$list_organize[] = $data;
		}

		$this->wrap_put_contents( $list_organize_path, $this->wrap_serialize($list_organize) );
		$qahm_log->info( '対象URL数: ' . count($list_organize), 'qalog_subcron.txt' );

		// 結果を確認（オプション）
		echo '<pre>';
//		echo print_r($list_organize, true);
		echo '</pre>';

		return true;
	}

	
	public function update_version_hist() {
		global $wpdb;
		global $qahm_log;
		global $qahm_time;

		$data_dir           = $this->get_data_dir_path();
		$view_dir           = $data_dir . 'view/';
		$version_hist_dir   = $view_dir . 'all/version_hist/';
		$css_base_dir       = $version_hist_dir . 'css/';

		$list_path          = $data_dir . 'subcron_list_organize.php';
		$list_progress_path = $data_dir . 'subcron_list_progress.php';
		$page_progress_path = $data_dir . 'subcron_list_page_progress.php';

		$version_hist_table = $wpdb->prefix . 'qa_page_version_hist';

		$list = $this->wrap_unserialize( $this->wrap_get_contents($list_path) );
		if( ! $list ){
			//データ破損とみなし、一旦削除
			$this->wrap_delete($list_path);
			echo $list_path . "を読み込めませんでした。<br>";
			$qahm_log->error( $list_path . "を読み込めませんでした。", 'qalog_subcron.txt' );
			return true;
		}

		// 進捗の読み込み
		$list_idx       = file_exists($list_progress_path) ? (int)$this->wrap_get_contents($list_progress_path) : 0;
		$page_idx       = 0;
		$page_start_idx = file_exists($page_progress_path) ? (int)$this->wrap_get_contents($page_progress_path) : 0;

		if ($list_idx >= count($list)) {
			// 全てのURLが処理された場合は進捗をリセット
			$this->wrap_put_contents($list_progress_path, '0');
			echo "リスト内の全てのURLを捜査しました。<br>";
			$qahm_log->info( "リスト内の全てのURLを捜査しました。", 'qalog_subcron.txt' );
			return true;
		}

		// 処理するURLを取得
		$data = $list[$list_idx];
		$url = $data['url'];
		$tracking_id = $data['tracking_id'];

		echo "list_index: $list_idx<br>";
		echo "page_start_index: $page_start_idx<br>";
		echo "url: $url<br>";
		echo "tracking ID: $tracking_id<br>";
		$qahm_log->info( "list_index: $list_idx, page_start_index: $page_start_idx, url: $url, tracking id: $tracking_id", 'qalog_subcron.txt' );

		/*
			css保存ディレクトリ
			cssの比較処理はhas_page_changed関数内に存在しているが、2024/11/11現在コメントアウトしている
			理由は、cssの比較をしてもページが変わったかわからないから

			しかしhas_page_changed関数にcssディレクトリを渡す処理を旧subcronが継承しているので
			今回もそのままにしている
			↓
			2024/11/21 cssディレクトリの引数削除

			$css_dir = $css_base_dir . $tracking_id;
			if( !$this->wrap_mkdir( $css_dir ) ){
				echo "css保存子ディレクトリの作成失敗: " . $css_dir . "<br>";
				$qahm_log->error( "css保存子ディレクトリの作成失敗: " . $css_dir, 'qalog_subcron.txt' );
				return false;
			}

		*/

		// 各デバイスでcurl_getを実行し配列に格納
		// device_idの配列を取得
		$device_ids = array_column(QAHM_DEVICES, 'id');
		$device_names = array_column(QAHM_DEVICES, 'name');

		// device_idとdevice_nameを関連付けた連想配列を作成
		$devices_map = array_combine($device_ids, $device_names);

		$base_html_ary = [];
		foreach ($devices_map as $id => $name) {
			$base_html_ary[$id] = $this->curl_get($url, 10, 10, $name);
			if( ! $base_html_ary[$id] ) {
				echo "base_htmlの取得に失敗しました。device_id: " . $id . "<br>";
				$qahm_log->warning( "base_htmlの取得に失敗しました。device_id: " . $id, 'qalog_subcron.txt' );
			}
		}

		if (isset($data['page_ids']) && is_array($data['page_ids'])) {
			foreach ($data['page_ids'] as $page_id => $page_data) {
				if ( $page_idx < $page_start_idx ) {
					$page_idx++;
					continue;
				}
				echo "Page ID: $page_id<br>";
	
				if (isset($page_data['device_ids']) && is_array($page_data['device_ids'])) {
					foreach ($page_data['device_ids'] as $device_id => $version_id) {
						echo "Device ID: $device_id, Version ID: $version_id<br>";
						$version_file_path = $version_hist_dir . $version_id . '_version.php';
						$base_html = $base_html_ary[$device_id];

						// 必要な処理をここに追加
						$hist = $this->wrap_unserialize( $this->wrap_get_contents( $version_file_path ) );
						if ( $hist ) {

							if ( $hist[0]->base_html ) {
								if( $this->has_page_changed($hist[0]->base_html, $base_html, $url) ) {
									// ただし当日中2回は更新しない
									if ($qahm_time->today_str() != $qahm_time->xday_str(0,$hist[0]->insert_datetime)) {
										$this->refresh_version_for_dev( $page_id, $device_id, $base_html ) ;
										echo 'バージョンアップしました。page_id: ' . $page_id . ', page_index: ' . $page_idx . ', version_id: ' . $version_id . '<br>';
										$qahm_log->info( 'バージョンアップしました。page_id: ' . $page_id . ', page_index: ' . $page_idx . ', version_id: ' . $version_id, 'qalog_subcron.txt' );

									}
								}
							} else {
								if( !$base_html ){
									continue;
								}
								$hist[0]->base_html = $base_html;
								$this->wrap_put_contents( $version_file_path, $this->wrap_serialize( $hist ) );
								echo 'base_htmlを設定しました。page_id: ' . $page_id . ', page_index: ' . $page_idx . ', version_id: ' . $version_id . '<br>';
								$qahm_log->info( 'base_htmlを設定しました。page_id: ' . $page_id . ', page_index: ' . $page_idx . ', version_id: ' . $version_id, 'qalog_subcron.txt' );
							}

						} else {

							echo('versionファイルの読み込みに失敗しました。削除してバージョンアップします。version_file_path: ' . $version_file_path . '<br>');
							$qahm_log->error( 'versionファイルの読み込みに失敗しました。削除してバージョンアップします。version_file_path: ' . $version_file_path, 'qalog_subcron.txt' );

							// ファイルを削除してバージョンアップ
							$this->wrap_delete( $version_file_path );
							$this->refresh_version_for_dev( $page_id, $device_id, $base_html ) ;
							echo 'バージョンアップしました。page_id: ' . $page_id . ', page_index: ' . $page_idx . ', version_id: ' . $version_id . '<br>';
							$qahm_log->info( 'バージョンアップしました。page_id: ' . $page_id . ', page_index: ' . $page_idx . ', version_id: ' . $version_id, 'qalog_subcron.txt' );
							/*
							$query        = 'SELECT * FROM ' . $version_hist_table . ' WHERE version_id=%d';
							$version_hist = $wpdb->get_results( $wpdb->prepare( $query, $version_id ) );
							if ( $version_hist ) {
								$version_hist[0]->base_html = $base_html;
								$res = $this->wrap_put_contents( $version_file_path, $this->wrap_serialize( $version_hist ) );
								if ( $res ) {
									echo 'versionファイルを再作成しました。version_file_path: ' . $version_file_path . '<br>';
									$qahm_log->info( 'versionファイルを再作成しました。version_file_path: ' . $version_file_path, 'qalog_subcron.txt' );
								}
							}
							*/
							continue;

						}
					}
				}
				
				// 次回再開用の進捗を保存
				$page_idx++;
				$this->wrap_put_contents($page_progress_path, $page_idx);
				usleep(500000); // 0.5秒待機
			}
		}

		// 次回再開用の進捗を保存
		$this->wrap_put_contents($page_progress_path, 0);
		$this->wrap_put_contents($list_progress_path, $list_idx + 1);

		return false;
	}

	public function exec_subcron() {
		global $wpdb;
		global $wp_filesystem;
		global $qahm_log;
		global $qahm_data_api;

		$start_time = microtime(true); // 開始時刻を取得
	
		$data_dir = $this->get_data_dir_path();
		$lock_file_path = $data_dir . 'subcron_lock';
		$status_file_path = $data_dir . 'subcron_status';
		$lock_file_timeout = 60 * 10; // 10分以上経過していたらロックファイルを削除

		// ロックファイルを開く（存在しない場合は作成）
		$lock_fp = fopen($lock_file_path, 'c+');
		if (!$lock_fp) {
			echo 'ロックファイルを開くことができませんでした。';
			$qahm_log->error('ロックファイルを開くことができませんでした。', 'qalog_subcron.txt');
			return;
		}

		// 排他ロックを試みる
		if (!flock($lock_fp, LOCK_EX | LOCK_NB)) {
			echo '別のプロセスが実行中です。終了します。';
			$qahm_log->info('別のプロセスが実行中です。終了します。', 'qalog_subcron.txt');
			fclose($lock_fp);
			return;
		}

		// 古いロックファイルの削除をチェック
		clearstatcache(true, $lock_file_path); // ファイル情報をキャッシュからクリア
		$lock_file_age = time() - filemtime($lock_file_path);
		if ($lock_file_age > $lock_file_timeout) {
			unlink($lock_file_path);
			echo '古いロックファイルを検出し、削除しました。<br>';
			$qahm_log->info('古いロックファイルを検出し、削除しました。', 'qalog_subcron.txt');
			// 再度ロック取得を試みる
			if (!flock($lock_fp, LOCK_EX | LOCK_NB)) {
				echo '別のプロセスが再実行を開始しました。終了します。<br>';
				$qahm_log->info('別のプロセスが再実行を開始しました。終了します。', 'qalog_subcron.txt');
				fclose($lock_fp);
				return;
			}
		}

		// 現在のプロセスIDを記録
		$pid = getmypid();
		ftruncate($lock_fp, 0); // ファイル内容をクリア
		fwrite($lock_fp, $pid . PHP_EOL);
		fflush($lock_fp); // バッファをフラッシュ

		echo "ロックファイルに対する排他制御権限を取得しました。PID: $pid<br>";
		$qahm_log->info("ロックファイルに対する排他制御権限を取得しました。PID: $pid", 'qalog_subcron.txt');

		// シャットダウン時にロック解除とロックファイル削除を行う
		register_shutdown_function(function () use ($lock_fp, $lock_file_path, $pid) {
			global $qahm_log;
			flock($lock_fp, LOCK_UN); // ロック解除
			fclose($lock_fp); // ファイルを閉じる
			if (file_exists($lock_file_path)) {
				unlink($lock_file_path); // ロックファイル削除
				echo "シャットダウン時にロックファイルを削除しました。PID: $pid<br>";
				$qahm_log->info("シャットダウン時にロックファイルを削除しました。PID: $pid", 'qalog_subcron.txt');
			}
		});

		try {
			// 夜間Cron起動中はSubCronを実行しない
			$status = null;
			if ( $wp_filesystem->exists( $this->get_cron_status_path() ) ) {
				$status = $wp_filesystem->get_contents( $this->get_cron_status_path() );
			}
			if ( $status !== null && strpos($status, 'Night') !== false ) {
				echo '夜間Cronが実行中です。動作を停止します。<br>';
				$qahm_log->info('夜間Cronが実行中です。動作を停止します。', 'qalog_subcron.txt');
				return;
			}

			// 全てのサイトのHTML定期取得が「しない」になっていた場合は即終了
			$sitemanage = $qahm_data_api->get_sitemanage();
			if ( empty( $sitemanage ) ){
				echo 'sitemanageにて、取得対象となるサイトが存在しません。';
				$qahm_log->error( 'sitemanageにて、取得対象となるサイトが存在しません。', 'qalog_subcron.txt' );
				return;
			}

			$is_stop = true;
			foreach ($sitemanage as $row) {
				if ($row['get_base_html_periodic'] == 1) {
					$is_stop = false;
					break;
				}
			}
			if ( $is_stop ) {
				echo '全てのサイトのget_base_html_periodicが0になっています。終了します。<br>';
				$qahm_log->info('全てのサイトのget_base_html_periodicが0になっています。終了します。', 'qalog_subcron.txt');
				return;
			}

			// 現在の状態を読み込む
			$current_step = file_exists($status_file_path) ? file_get_contents($status_file_path) : 'Create Sub Cron List';
			$current_step_log = $current_step;
			// debug
			//$current_step = 'Organize Sub Cron List';
			echo $current_step . '<br>';
			$qahm_log->info( '開始: ' . $current_step, 'qalog_subcron.txt' );

			// ステップに応じた処理
			switch ($current_step) {
				case 'Create Sub Cron List':
					$state = $this->create_subcron_list();
					if ( $state ) {
						$current_step = 'Organize Sub Cron List';
					}
					$this->set_next_status($current_step);
					break;

				case 'Organize Sub Cron List':
					$state = $this->organize_subcron_list();
					if ( $state ) {
						$current_step = 'Update Version Hist';
					} else {
						$current_step = 'Create Sub Cron List';
					}
					$this->set_next_status($current_step);
					break;
	
				case 'Update Version Hist':
					$state = $this->update_version_hist();
					if ( $state ) {
						$current_step = 'Create Sub Cron List';
					}
					$this->set_next_status($current_step);
					break;
	
				default:
					// $status_file_pathを削除
					unlink($status_file_path);
					break;
			}
	
		} catch (Throwable $e) {
			// エラーハンドリングとログ出力
			echo 'Catch, ' . basename( $e->getFile() ) . ':' . $e->getLine() . ', ' . $e->getMessage() . '<br>';
			$qahm_log->error( 'Catch, ' . basename( $e->getFile() ) . ':' . $e->getLine() . ', ' . $e->getMessage(), 'qalog_subcron.txt' );
		}
	
		$end_time = microtime(true); // 終了時刻を取得
		$execution_time = $end_time - $start_time; // 実行時間を計算
	
		// 実行時間を表示
		echo "Execution time: " . round($execution_time, 4) . " seconds\n";
		
		$qahm_log->info( '終了: ' . $current_step_log, 'qalog_subcron.txt' );
	}
	
	private function set_next_status($next_status) {
		global $wp_filesystem;
		$data_dir = $this->get_data_dir_path();
		$status_file_path = $data_dir . 'subcron_status';
		if (!$wp_filesystem->put_contents($status_file_path, $next_status)) {
			throw new Exception($next_status . 'のセットでcronステータスファイルの書込に失敗しました。終了します。');
		}
	}

	/**
	 * デバイス毎のバージョンアップ
	 */
	public function refresh_version_for_dev( $page_id, $device_id, $base_html ) {
		global $qahm_db;
		global $qahm_time;
		global $wpdb;
		global $wp_filesystem;

		//不正パラメーター判断
		if ( ! $base_html || ! $page_id || ! $device_id ) {
			return false;
		}
		//page_idが存在するか？
		$table_name = $qahm_db->prefix . 'qa_pages';
		$query      = 'SELECT page_id FROM ' . $table_name . ' WHERE page_id = %d';
		$qa_page_id = $qahm_db->get_results( $qahm_db->prepare( $query, $page_id ) );
		if ( ! $qa_page_id ) {
			return false;
		}
		//device_idが存在するか？
		if ( $device_id < QAHM_DEVICES['desktop']['id']  || QAHM_DEVICES['smartphone']['id'] < $device_id ) {
			return false;
		}
		$data_dir          = $this->get_data_dir_path();
		$view_dir          = $data_dir . 'view/';
		$myview_dir        = $view_dir . $this->get_tracking_id(). '/';
		$vw_verhst_dir     = $myview_dir . 'version_hist/';

		$today_str = $qahm_time->today_str();
		$now_str   = $qahm_time->now_str();

		$pageid_in_num = floor( $page_id / QAHM_Cron_Proc::ID_INDEX_MAX10MAN );
		$start_index       = $pageid_in_num * QAHM_Cron_Proc::ID_INDEX_MAX10MAN + 1;
		$end_index         = $start_index + QAHM_Cron_Proc::ID_INDEX_MAX10MAN - 1;
		$pageid_index_file = $start_index . '-' . $end_index . '_pageid.php';

		//まず最初に最新のversion_noをDBから取得。QA ZEROでは1年間DBに保持される。
		//versionを全てひっぱってくる
		$table_name           = $qahm_db->prefix . 'qa_page_version_hist';
		$query                = 'SELECT version_no FROM ' . $table_name . ' WHERE page_id=%d AND device_id=%d';
		$version_no_ary       = $qahm_db->get_results( $qahm_db->prepare( $query, $page_id, $device_id), ARRAY_A );

		$cur_ver_no = 1;
		if ( $version_no_ary ) {
			foreach ( $version_no_ary as $ver_no ) {
				if ( $cur_ver_no < $ver_no['version_no'] ) {
					$cur_ver_no = $ver_no['version_no'];
				}
			}
		} else {
			//ファイルからバージョンを取得する
			if ( ! is_file($vw_verhst_dir . 'index/' . $pageid_index_file ) ) {
				$this->remake_indexfile_from_versionfile( $pageid_index_file );
			}
			$table_name           = $qahm_db->prefix . 'view_page_version_hist';
			$query                = 'SELECT version_id,device_id,version_no FROM ' . $table_name . ' WHERE page_id = %d';
			$qa_page_version_hist = $qahm_db->get_results( $qahm_db->prepare( $query, $page_id ) );
			if ( $qa_page_version_hist ) {
				foreach ( $qa_page_version_hist as $hist ) {
					if ( $cur_ver_no < (int)$hist['version_no'] && (int)$hist['device_id'] === (int)$device_id ) {
						$cur_ver_no = (int)$hist['version_no'];
					}
				}
			}
		}
		$new_ver_no = $cur_ver_no + 1;

		// バージョン追加
		$ver_id               = 0;
		$table_name           = $qahm_db->prefix . 'qa_page_version_hist';
		$query                = 'SELECT version_id FROM ' . $table_name . ' WHERE page_id=%d AND device_id=%d AND version_no=%d';
		$already_version_hist = $qahm_db->get_results( $qahm_db->prepare( $query, $page_id, $device_id, $new_ver_no ) );
		//version_idが存在ない場合はInsert
		if ( $already_version_hist ) {
			$ver_id = (int)$already_version_hist[0]->version_id;
		} else {
			$query = 'INSERT INTO ' . $table_name . ' ' .
					'(page_id, device_id, version_no, base_html, update_date, insert_datetime) ' .
					'VALUES( %d, %d, %d, %s, %s, %s )';
			$result = $qahm_db->query( $qahm_db->prepare( $query, $page_id, $device_id, $new_ver_no, "", $today_str, $now_str ) );
			if ($result !== false) {
				$ver_id = (int)$wpdb->insert_id;
			}
		}
		//version_idがあれば作成
		$makever = false;
		if ( $ver_id !== 0 ) {
			$newfile = $ver_id . '_version.php';
			//version array はフォーマットをミスったまま仕様になったので0番目になっている。かつget_results標準形式なので文字列だけのオブジェクトが入っているので注意
			$verary    = [];
			$verary[0] = (object)['version_id' => (string)$ver_id, 'page_id' => (string)$page_id, 'device_id' => (string)$device_id, 'version_no' => (string)$new_ver_no, 'base_html' => $base_html, 'base_selector' => null, 'update_date' => $today_str, 'insert_datetime' => $now_str];
			$makever   = $this->wrap_put_contents( $vw_verhst_dir . $newfile, $this->wrap_serialize( $verary ) );

			//indexファイルの更新
			//最初のpage_id用のindex配列を作る。page_id用の配列はIDですぐ飛べるように、1スタートの固定長にする。
			if ( $wp_filesystem->exists( $vw_verhst_dir . 'index/' ) ) {
				if ( is_file($vw_verhst_dir . 'index/' . $pageid_index_file ) ) {
					$verhst_pageid_index = $this->wrap_unserialize( $this->wrap_get_contents( $vw_verhst_dir . 'index/' . $pageid_index_file ) );
					$verhst_pageid_index[$page_id][] = $ver_id;
					$makever = $this->wrap_put_contents( $vw_verhst_dir . 'index/' . $pageid_index_file, $this->wrap_serialize( $verhst_pageid_index ) );
				} else {
					$this->remake_indexfile_from_versionfile( $pageid_index_file);
				}
			} else {
				$wp_filesystem->mkdir( $vw_verhst_dir . 'index/' );
				$this->remake_indexfile_from_versionfile( $pageid_index_file);
			}
		}

		$retval = $new_ver_no;
		if ( $makever === false ) {
			$retval = false;
		}
		return $retval;
	}
	/**
	 * versionファイルのindex再作成
	 */
	public function remake_indexfile_from_versionfile( $pageid_index_file ) {
		$data_dir          = $this->get_data_dir_path();
		$view_dir          = $data_dir . 'view/';
		$myview_dir        = $view_dir . $this->get_tracking_id(). '/';
		$vw_verhst_dir     = $myview_dir . 'version_hist/';
		$vw_verhst_index   = $vw_verhst_dir . 'index/';
		$version_dirs = $this->wrap_dirlist( $vw_verhst_dir );
		if ( ! $version_dirs ) {
			return false;
		}
		$verhst_pageid_index = [];
		$page_ids = explode( '-', $pageid_index_file );
		$start_page_id = (int)$page_ids[0];
		$end_page_id   = (int)$page_ids[1];
		foreach ( $version_dirs as $version_file ) {
			if ( is_file( $vw_verhst_dir . $version_file['name'] ) ) {
				$version_file_name = $version_file['name'];
				$version_file_path = $vw_verhst_dir . $version_file_name;
				$version_file_ary  = $this->wrap_unserialize( $this->wrap_get_contents( $version_file_path ) );
				if ( $version_file_ary ) {
					foreach ( $version_file_ary as $version ) {
						$page_id = (int)$version->page_id;
						if ( $start_page_id <= $page_id && $page_id <= $end_page_id ) {
							$versionstrs                       = explode( '_', $version_file_name );
							$ver_id                            = (int)$versionstrs[0];
							$verhst_pageid_index[ $page_id ][] = $ver_id;
						}
					}
				}
			}
		}
		if ( $verhst_pageid_index ) {
			$this->wrap_put_contents( $vw_verhst_index . $pageid_index_file, $this->wrap_serialize( $verhst_pageid_index ) );
		}
		return $verhst_pageid_index;
	}

}

?>