<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * 天気情報取得と AI プロンプト用テキスト生成。
 *
 * Open-Meteo JMA API を使用する。API キーは不要。
 * 取得失敗時は投稿処理を止めず、AI に天気表現を断定しない指示だけを返す。
 */
class AIAP_Weather_Client {

    const CACHE_PREFIX = 'aiap_weather_context_v2_';

    public static function default_settings() {
        return array(
            'weather_enabled'       => 0,
            'weather_location_name' => '',
            'weather_latitude'      => '',
            'weather_longitude'     => '',
            'weather_cache_minutes' => 60,
        );
    }

    public static function sanitize_settings( array $settings ) {
        $settings['weather_enabled'] = isset( $_POST['weather_enabled'] ) ? 1 : 0;

        $location = sanitize_text_field( wp_unslash( $_POST['weather_location_name'] ?? '' ) );
        $settings['weather_location_name'] = $location !== '' ? AIAP_Compat::mb_substr( $location, 0, 80 ) : '';

        $lat = self::sanitize_coordinate( wp_unslash( $_POST['weather_latitude'] ?? '' ), -90, 90 );
        $lon = self::sanitize_coordinate( wp_unslash( $_POST['weather_longitude'] ?? '' ), -180, 180 );
        $settings['weather_latitude']  = $lat;
        $settings['weather_longitude'] = $lon;

        $settings['weather_cache_minutes'] = max( 15, min( 180, absint( wp_unslash( $_POST['weather_cache_minutes'] ?? 60 ) ) ) );
        return $settings;
    }


    public static function validate_settings( array $settings ) {
        if ( empty( $settings['weather_enabled'] ) ) {
            return true;
        }

        $location = trim( (string) ( $settings['weather_location_name'] ?? '' ) );
        $lat      = trim( (string) ( $settings['weather_latitude'] ?? '' ) );
        $lon      = trim( (string) ( $settings['weather_longitude'] ?? '' ) );

        if ( $location === '' || $lat === '' || $lon === '' ) {
            return new WP_Error(
                'aiap_weather_required_missing',
                __( 'Weather information is enabled. Enter the location name, latitude, and longitude.', 'ai-auto-poster' )
            );
        }

        if ( ! is_numeric( $lat ) || ! is_numeric( $lon ) ) {
            return new WP_Error(
                'aiap_weather_coordinate_invalid',
                __( 'Weather information is enabled. Enter valid numeric latitude and longitude.', 'ai-auto-poster' )
            );
        }

        return true;
    }


    public static function geocode_location( $query ) {
        $query = trim( sanitize_text_field( (string) $query ) );
        if ( $query === '' ) {
            return new WP_Error( 'aiap_weather_geocode_empty', __( 'Enter a location name.', 'ai-auto-poster' ) );
        }

        $cache_key = 'aiap_weather_geocode_' . md5( AIAP_Compat::mb_strtolower( $query ) );
        $cached = get_transient( $cache_key );
        if ( is_array( $cached ) ) {
            return $cached;
        }

        $url = add_query_arg(
            array(
                'name'     => $query,
                'count'    => 1,
                'language' => self::geocoding_language(),
                'format'   => 'json',
            ),
            'https://geocoding-api.open-meteo.com/v1/search'
        );

        $response = wp_remote_get( $url, array( 'timeout' => 8 ) );
        if ( is_wp_error( $response ) ) {
            return $response;
        }

        $code = (int) wp_remote_retrieve_response_code( $response );
        if ( $code < 200 || $code >= 300 ) {
            return new WP_Error( 'aiap_weather_geocode_http_error', __( 'Could not find the location. Enter the location again.', 'ai-auto-poster' ) );
        }

        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        if ( ! is_array( $body ) || empty( $body['results'] ) || ! is_array( $body['results'] ) ) {
            return new WP_Error( 'aiap_weather_geocode_not_found', __( 'Could not find the location. Enter the location again.', 'ai-auto-poster' ) );
        }

        $result = reset( $body['results'] );
        if ( ! is_array( $result ) || ! isset( $result['latitude'], $result['longitude'] ) ) {
            return new WP_Error( 'aiap_weather_geocode_invalid_response', __( 'Could not find the location. Enter the location again.', 'ai-auto-poster' ) );
        }

        $lat = self::sanitize_coordinate( $result['latitude'], -90, 90 );
        $lon = self::sanitize_coordinate( $result['longitude'], -180, 180 );
        if ( $lat === '' || $lon === '' ) {
            return new WP_Error( 'aiap_weather_geocode_invalid_coordinate', __( 'Could not find the location. Enter the location again.', 'ai-auto-poster' ) );
        }

        $label_parts = array();
        foreach ( array( 'name', 'admin1', 'country' ) as $key ) {
            if ( ! empty( $result[ $key ] ) && is_scalar( $result[ $key ] ) ) {
                $part = sanitize_text_field( (string) $result[ $key ] );
                if ( $part !== '' && ! in_array( $part, $label_parts, true ) ) {
                    $label_parts[] = $part;
                }
            }
        }

        $data = array(
            'latitude'  => $lat,
            'longitude' => $lon,
            'label'     => implode( ', ', $label_parts ),
        );

        set_transient( $cache_key, $data, DAY_IN_SECONDS );
        return $data;
    }

    private static function geocoding_language() {
        $locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
        $lang = strtolower( substr( (string) $locale, 0, 2 ) );
        return preg_match( '/^[a-z]{2}$/', $lang ) ? $lang : 'ja';
    }

    private static function sanitize_coordinate( $value, $min, $max ) {
        $value = trim( (string) $value );
        if ( $value === '' || ! is_numeric( $value ) ) {
            return '';
        }
        $float = (float) $value;
        if ( $float < $min || $float > $max ) {
            return '';
        }
        return rtrim( rtrim( sprintf( '%.6F', $float ), '0' ), '.' );
    }

    public static function build_prompt_context( array $settings ) {
        if ( empty( $settings['weather_enabled'] ) ) {
            return '';
        }

        $location = trim( (string) ( $settings['weather_location_name'] ?? '' ) );
        $lat      = trim( (string) ( $settings['weather_latitude'] ?? '' ) );
        $lon      = trim( (string) ( $settings['weather_longitude'] ?? '' ) );

        if ( $location === '' || ! is_numeric( $lat ) || ! is_numeric( $lon ) ) {
            return self::unknown_weather_context( $location );
        }

        $data = self::get_weather( $settings );
        if ( is_wp_error( $data ) ) {
            return self::unknown_weather_context( $location );
        }

        return self::format_for_prompt( $location, $data );
    }

    private static function get_weather( array $settings ) {
        $lat = trim( (string) ( $settings['weather_latitude'] ?? '' ) );
        $lon = trim( (string) ( $settings['weather_longitude'] ?? '' ) );
        if ( ! is_numeric( $lat ) || ! is_numeric( $lon ) ) {
            return new WP_Error( 'aiap_weather_missing_coordinates', 'Weather coordinates are not configured.' );
        }
        $cache_minutes = max( 15, min( 180, (int) ( $settings['weather_cache_minutes'] ?? 60 ) ) );
        // Bug fix: キャッシュキーに当日 (サイトタイムゾーンの日付) を含める。
        // 旧実装は md5(lat,lon) のみで日付を持たず、TTL (最大3時間) が日跨ぎで生き残ると
        // 前日に取得した daily 配列をそのまま再利用していた。daily の各要素を昨日/今日/明日へ
        // 割り当てるのは「取得日」基準のため、日跨ぎで「今日」の枠に前日のデータが入り、
        // 晴れの日に前日(雨)の天気が注入される原因になっていた。日付を鍵に含めれば、
        // 日付が変わった時点でキャッシュミス → 再取得され、割り当てが常に当日基準になる。
        $today_ymd = ( new DateTimeImmutable( 'now', wp_timezone() ) )->format( 'Y-m-d' );
        $cache_key = self::CACHE_PREFIX . md5( $lat . ',' . $lon . ',' . $today_ymd );

        $cached = get_transient( $cache_key );
        if ( is_array( $cached ) ) {
            return $cached;
        }

        $url = add_query_arg(
            array(
                'latitude'       => $lat,
                'longitude'      => $lon,
                'hourly'         => 'temperature_2m,relative_humidity_2m,precipitation,weather_code,wind_speed_10m',
                'daily'          => 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum',
                'timezone'       => 'auto',
                'past_days'      => 1,
                'forecast_days'  => 2,
            ),
            'https://api.open-meteo.com/v1/jma'
        );

        $response = wp_remote_get( $url, array( 'timeout' => 8 ) );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = (int) wp_remote_retrieve_response_code( $response );
        if ( $code < 200 || $code >= 300 ) {
            return new WP_Error( 'aiap_weather_http_error', 'Weather API HTTP error: ' . $code );
        }

        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        if ( ! is_array( $body ) || empty( $body['hourly'] ) || empty( $body['daily'] ) ) {
            return new WP_Error( 'aiap_weather_invalid_response', 'Weather API response is invalid.' );
        }

        set_transient( $cache_key, $body, $cache_minutes * MINUTE_IN_SECONDS );
        return $body;
    }

    private static function format_for_prompt( $location, array $data ) {
        $daily  = isset( $data['daily'] ) && is_array( $data['daily'] ) ? $data['daily'] : array();
        $hourly = isset( $data['hourly'] ) && is_array( $data['hourly'] ) ? $data['hourly'] : array();

        // Bug fix: 昨日/今日/明日を固定インデックス 0/1/2 ではなく daily['time'] の実日付で
        // 解決する。past_days=1/forecast_days=2 のため取得直後は [昨日,今日,明日] の順だが、
        // インデックス前提だと取得日と利用日がずれた際に割り当てが崩れる。実日付で引けば
        // キャッシュの取得タイミングに依らず常に正しい日付のデータを割り当てられる。
        $tz_string = is_string( $data['timezone'] ?? '' ) ? trim( (string) $data['timezone'] ) : '';
        try {
            $tz = $tz_string !== '' ? new DateTimeZone( $tz_string ) : wp_timezone();
        } catch ( Exception $e ) {
            $tz = wp_timezone();
        }
        $today_dt = new DateTimeImmutable( 'now', $tz );
        $yesterday_weather = self::daily_summary_by_date( $daily, $today_dt->modify( '-1 day' )->format( 'Y-m-d' ) );
        $today_weather     = self::daily_summary_by_date( $daily, $today_dt->format( 'Y-m-d' ) );
        $tomorrow_weather  = self::daily_summary_by_date( $daily, $today_dt->modify( '+1 day' )->format( 'Y-m-d' ) );

        $now_index = self::current_hour_index( $hourly, $data['timezone'] ?? '' );
        $humidity  = self::array_at( $hourly, 'relative_humidity_2m', $now_index );
        $wind      = self::array_at( $hourly, 'wind_speed_10m', $now_index );
        $now_temp  = self::array_at( $hourly, 'temperature_2m', $now_index );
        $now_rain  = self::array_at( $hourly, 'precipitation', $now_index );

        // 今日の時間帯別の降水予報。daily の代表 weather_code は「1日に1コード」のため、
        // 日中は晴れていて夕方だけ弱い雨、のような日も daily では「弱い雨」に丸められる。
        // 体感とのズレ(晴れなのに記事が雨)を防ぐため、何時頃にどれだけ降る予報かを
        // 事実として併記し、AI が日中と降水時間帯を区別して書けるようにする。
        $today_rain_line = self::today_precipitation_line( $hourly, $today_weather['date'] );

        // 天気の「事実データ」のみを渡す。雨・季節・気温などをどう描写する/しないか
        // (旧「使用ルール」「厳格な雨ルール」) は本プラグインの責務ではなくシステム
        // プロンプト側の責務のため、プラグインからは指示文を出さない (関心の分離)。
        $lines = array();
        $lines[] = '【天気コンテキスト】';
        $lines[] = '対象地域：' . $location;
        $lines[] = '情報源：Open-Meteo JMA forecast';
        $lines[] = '';
        $lines = array_merge( $lines, self::format_daily_lines( '昨日の天気', $yesterday_weather ) );
        $lines[] = '';
        $lines = array_merge( $lines, self::format_daily_lines( '今日の天気', $today_weather ) );
        if ( $today_rain_line !== '' ) {
            $lines[] = $today_rain_line;
        }
        if ( is_numeric( $now_temp ) ) {
            $lines[] = '現在または直近時刻の気温：' . round( (float) $now_temp, 1 ) . '℃';
        }
        if ( is_numeric( $now_rain ) ) {
            $lines[] = '現在または直近時刻の降水量：' . round( (float) $now_rain, 1 ) . 'mm';
        }
        if ( is_numeric( $humidity ) ) {
            $lines[] = '湿度：' . (int) round( (float) $humidity ) . '%';
        }
        if ( is_numeric( $wind ) ) {
            $lines[] = '風速：' . round( (float) $wind, 1 ) . 'km/h';
        }
        $lines[] = '';
        $lines = array_merge( $lines, self::format_daily_lines( '明日の天気', $tomorrow_weather ) );

        return implode( "\n", $lines );
    }

    private static function daily_summary_by_date( array $daily, $date ) {
        // daily['time'] (= 'Y-m-d' の配列) から指定日付のインデックスを引く。
        // 見つからなければ空サマリ (format_daily_lines が「日付：」空欄等で安全に描画)。
        $times = ( isset( $daily['time'] ) && is_array( $daily['time'] ) ) ? $daily['time'] : array();
        $index = array_search( (string) $date, $times, true );
        if ( $index === false ) {
            return array(
                'date'     => '',
                'code'     => null,
                'max_temp' => null,
                'min_temp' => null,
                'rain_sum' => null,
            );
        }
        $found = self::array_at( $daily, 'time', $index );
        return array(
            'date'      => is_scalar( $found ) ? (string) $found : '',
            'code'      => self::array_at( $daily, 'weather_code', $index ),
            'max_temp'  => self::array_at( $daily, 'temperature_2m_max', $index ),
            'min_temp'  => self::array_at( $daily, 'temperature_2m_min', $index ),
            'rain_sum'  => self::array_at( $daily, 'precipitation_sum', $index ),
        );
    }

    private static function format_daily_lines( $title, array $summary ) {
        $lines = array();
        $lines[] = '【' . $title . '】';
        $lines[] = '日付：' . $summary['date'];
        $lines[] = '天気：' . self::weather_code_to_japanese( $summary['code'] );
        if ( is_numeric( $summary['max_temp'] ) ) {
            $lines[] = '最高気温：' . round( (float) $summary['max_temp'], 1 ) . '℃';
        }
        if ( is_numeric( $summary['min_temp'] ) ) {
            $lines[] = '最低気温：' . round( (float) $summary['min_temp'], 1 ) . '℃';
        }
        if ( is_numeric( $summary['rain_sum'] ) ) {
            $lines[] = '降水量：' . round( (float) $summary['rain_sum'], 1 ) . 'mm';
        }
        return $lines;
    }

    /**
     * 今日の時間帯別降水を要約した1行を返す (事実のみ。指示は含めない)。
     *
     * daily の代表 weather_code は1日1コードのため、夕方だけ弱い雨の日でも「弱い雨」に
     * 丸められ、AI が日中も雨と誤認しやすい。hourly['precipitation'] から実際に降る
     * 時間帯と量を抽出し、降らない時間帯も明示することで、AI が日中と降水時間帯を
     * 区別して記述できるようにする。
     *
     * @param array  $hourly      Open-Meteo hourly オブジェクト
     * @param string $today_date  対象日 ('Y-m-d')。空なら判定不能として空文字を返す。
     * @return string 例: 「今日の降水予報：17時〜18時頃に弱い雨(計3.1mm)、その他の時間帯は降水なし」
     */
    private static function today_precipitation_line( array $hourly, $today_date ) {
        $today_date = (string) $today_date;
        if ( $today_date === '' ) {
            return '';
        }
        $times = ( isset( $hourly['time'] ) && is_array( $hourly['time'] ) ) ? $hourly['time'] : array();
        $precs = ( isset( $hourly['precipitation'] ) && is_array( $hourly['precipitation'] ) ) ? $hourly['precipitation'] : array();
        if ( empty( $times ) ) {
            return '';
        }

        $today_hours_total = 0; // 今日の時間サンプル数 (データ有無の判定用)
        $rain_hours        = array(); // 降水のある「時」(0-23) のリスト
        $rain_total        = 0.0;
        foreach ( $times as $i => $t ) {
            if ( ! is_string( $t ) || strpos( $t, $today_date ) !== 0 ) {
                continue;
            }
            $today_hours_total++;
            $p = isset( $precs[ $i ] ) && is_numeric( $precs[ $i ] ) ? (float) $precs[ $i ] : 0.0;
            if ( $p > 0 ) {
                $hour = (int) substr( $t, 11, 2 );
                $rain_hours[] = $hour;
                $rain_total  += $p;
            }
        }

        if ( $today_hours_total === 0 ) {
            return ''; // 今日の時間別データが無い (取得期間外等) → 何も主張しない
        }
        if ( empty( $rain_hours ) ) {
            return '今日の降水予報：日中を通して降水なし';
        }

        sort( $rain_hours );
        $ranges = self::compress_hour_ranges( $rain_hours );
        return '今日の降水予報：' . implode( '、', $ranges )
            . '頃に降水あり(計' . round( $rain_total, 1 ) . 'mm)、その他の時間帯は降水なし';
    }

    /**
     * 連続する「時」をまとめて「17時〜18時」「9時」のような範囲文字列の配列にする。
     */
    private static function compress_hour_ranges( array $hours ) {
        $ranges = array();
        $start  = null;
        $prev   = null;
        foreach ( $hours as $h ) {
            if ( $start === null ) {
                $start = $h;
                $prev  = $h;
                continue;
            }
            if ( $h === $prev + 1 ) {
                $prev = $h;
                continue;
            }
            $ranges[] = ( $start === $prev ) ? ( $start . '時' ) : ( $start . '時〜' . $prev . '時' );
            $start = $h;
            $prev  = $h;
        }
        if ( $start !== null ) {
            $ranges[] = ( $start === $prev ) ? ( $start . '時' ) : ( $start . '時〜' . $prev . '時' );
        }
        return $ranges;
    }

    private static function unknown_weather_context( $location ) {
        // 天気を取得できなかったケース。指示文は出さず「取得不可」という事実のみ渡す
        // (どう扱うかはシステムプロンプトの責務 — 成功時の format_for_prompt と方針を揃える)。
        $location = trim( (string) $location );
        if ( $location === '' ) {
            $location = '設定地域';
        }
        return "【天気コンテキスト】\n対象地域：" . $location . "\n情報源：取得不可";
    }

    private static function array_first( array $data, $key ) {
        if ( isset( $data[ $key ] ) && is_array( $data[ $key ] ) && array_key_exists( 0, $data[ $key ] ) ) {
            return $data[ $key ][0];
        }
        return null;
    }

    private static function array_at( array $data, $key, $index ) {
        if ( isset( $data[ $key ] ) && is_array( $data[ $key ] ) && array_key_exists( $index, $data[ $key ] ) ) {
            return $data[ $key ][ $index ];
        }
        return null;
    }

    private static function current_hour_index( array $hourly, $timezone ) {
        if ( empty( $hourly['time'] ) || ! is_array( $hourly['time'] ) ) {
            return 0;
        }
        $timezone = is_string( $timezone ) ? trim( $timezone ) : '';
        try {
            $tz = $timezone !== '' ? new DateTimeZone( $timezone ) : wp_timezone();
        } catch ( Exception $e ) {
            $tz = wp_timezone();
        }
        $now = ( new DateTimeImmutable( 'now', $tz ) )->format( 'Y-m-d\TH:00' );
        $idx = array_search( $now, $hourly['time'], true );
        return $idx === false ? 0 : (int) $idx;
    }

    private static function weather_code_to_japanese( $code ) {
        $code = is_numeric( $code ) ? (int) $code : -1;
        $map = array(
            0 => '快晴', 1 => '晴れ', 2 => '一部曇り', 3 => '曇り',
            45 => '霧', 48 => '着氷性の霧',
            51 => '弱い霧雨', 53 => '霧雨', 55 => '強い霧雨',
            56 => '弱い着氷性の霧雨', 57 => '着氷性の霧雨',
            61 => '弱い雨', 63 => '雨', 65 => '強い雨',
            66 => '弱い着氷性の雨', 67 => '着氷性の雨',
            71 => '弱い雪', 73 => '雪', 75 => '強い雪', 77 => '雪粒',
            80 => '弱いにわか雨', 81 => 'にわか雨', 82 => '強いにわか雨',
            85 => '弱いにわか雪', 86 => '強いにわか雪',
            95 => '雷雨', 96 => '雹を伴う雷雨', 99 => '強い雹を伴う雷雨',
        );
        return isset( $map[ $code ] ) ? $map[ $code ] : '不明';
    }
}
