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

/** Built-in, same-site URL shortener used by the publication pipeline. */
final class Calliope_SP_Short_Link_Service {
    public const ENABLED_OPTION = 'calliope_sp_short_links_enabled';
    public const REWRITE_VERSION_OPTION = 'calliope_sp_short_links_rewrite_version';
    public const REWRITE_VERSION = '2';
    private const QUERY_VAR = 'calliope_sp_short_code';
    private const CODE_LENGTH = 8;

    public static function is_enabled(): bool {
        return get_option(self::ENABLED_OPTION, '1') !== '0';
    }

    public static function register_rewrite(): void {
        add_rewrite_rule('^sp/([A-Za-z0-9]{6,10})/?$', 'index.php?' . self::QUERY_VAR . '=$matches[1]', 'top');
    }

    public static function register_query_var(array $vars): array {
        $vars[] = self::QUERY_VAR;
        return array_values(array_unique($vars));
    }

    public static function maybe_flush_rewrite_rules(): void {
        if ((string) get_option(self::REWRITE_VERSION_OPTION, '') === self::REWRITE_VERSION) { return; }
        self::register_rewrite();
        flush_rewrite_rules(false);
        update_option(self::REWRITE_VERSION_OPTION, self::REWRITE_VERSION, false);
        // Remove the legacy TikTok OAuth rewrite marker. TikTok is not a supported destination in the 2.x Free line.
        delete_option('calliope_spp_tiktok_rewrite_version');
    }

    public static function activate_rewrite(bool $network_wide = false): void {
        if (is_multisite() && $network_wide) {
            $offset=0; $batch=100;
            do {
                $ids=get_sites(['fields'=>'ids','number'=>$batch,'offset'=>$offset,'orderby'=>'id','order'=>'ASC']);
                foreach ((array)$ids as $site_id) {
                    switch_to_blog((int)$site_id);
                    try {
                        self::register_rewrite();
                        flush_rewrite_rules(false);
                        update_option(self::REWRITE_VERSION_OPTION,self::REWRITE_VERSION,false);
                    } finally { restore_current_blog(); }
                }
                $count=is_array($ids)?count($ids):0; $offset+=$count;
            } while ($count===$batch);
            return;
        }
        self::register_rewrite();
        flush_rewrite_rules(false);
        update_option(self::REWRITE_VERSION_OPTION,self::REWRITE_VERSION,false);
    }

    public static function handle_redirect(): void {
        $code = (string) get_query_var(self::QUERY_VAR, '');
        if ($code === '') { return; }
        $row = self::find_by_code($code);
        if (!$row || (string) ($row['status'] ?? '') !== 'active') {
            self::render_404();
            return;
        }
        $target = self::normalize_target_url((string) ($row['target_url'] ?? ''));
        if ($target === '') {
            self::render_404();
            return;
        }
        // Redirects are intentionally not counted or profiled in this step.
        if (!wp_safe_redirect($target, 302, 'Social Publisher')) {
            self::render_404();
            return;
        }
        exit;
    }

    public static function get_or_create(string $url) {
        $target = self::normalize_target_url($url);
        if ($target === '') {
            return new WP_Error('calliope_sp_short_link_invalid_target', __('Invalid short link', 'calliope-social-publisher'));
        }
        Calliope_SP_Short_Link_Schema::maybe_upgrade();
        $hash = hash('sha256', $target);
        $existing = self::find_by_target_hash($hash);
        if ($existing && (string) ($existing['status'] ?? '') === 'active') {
            self::touch((int) ($existing['id'] ?? 0));
            return self::short_url((string) ($existing['code'] ?? ''));
        }

        global $wpdb;
        $table = Calliope_SP_Short_Link_Schema::table_name();
        $now = gmdate('Y-m-d H:i:s');
        for ($attempt=0; $attempt<12; $attempt++) {
            $code = self::generate_code();
            $uuid = function_exists('wp_generate_uuid4') ? wp_generate_uuid4() : self::uuid_fallback();
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned short-link mapping is intentionally persisted here.
            $inserted = $wpdb->insert($table, [
                'uuid'=>$uuid,
                'code'=>$code,
                'target_url'=>$target,
                'target_hash'=>$hash,
                'status'=>'active',
                'created_at_utc'=>$now,
                'updated_at_utc'=>$now,
                'last_used_at_utc'=>$now,
            ], ['%s','%s','%s','%s','%s','%s','%s','%s']);
            if ($inserted) { return self::short_url($code); }
            $existing = self::find_by_target_hash($hash);
            if ($existing && (string) ($existing['status'] ?? '') === 'active') {
                return self::short_url((string) ($existing['code'] ?? ''));
            }
        }
        return new WP_Error('calliope_sp_short_link_store_failed', __('The short link could not be created.', 'calliope-social-publisher'));
    }

    public static function existing_short_url(string $url): string {
        $target = self::normalize_target_url($url);
        if ($target === '') { return ''; }
        $row = self::find_by_target_hash(hash('sha256',$target));
        return $row && (string)($row['status']??'')==='active' ? self::short_url((string)($row['code']??'')) : '';
    }

    public static function preview_short_url(string $url): string {
        // Preview must stay side-effect free: do not touch the schema or create/read mappings.
        if (self::normalize_target_url($url) === '') { return ''; }
        return untrailingslashit(home_url('/')) . '/sp/••••••';
    }

    /** @return array{enabled:bool,links:array<string,string>} */
    public static function build_schedule_snapshot(array $data, array $destinations, int $post_id=0): array {
        $existing = self::sanitize_schedule_snapshot($data['short_link_snapshot'] ?? []);
        if ($existing !== []) { return $existing; }
        $enabled = self::is_enabled();
        if (!$enabled) { return ['enabled'=>false,'links'=>[]]; }
        $links=[];
        foreach (array_values(array_unique(array_map('sanitize_key',$destinations))) as $network) {
            if ($network==='') { continue; }
            $payload=$data;
            $context=['post_id'=>max(0,$post_id),'short_link_snapshot_build'=>1];
            if (class_exists('Calliope_SP_Tracking')) {
                $payload=Calliope_SP_Tracking::apply_to_payload($payload,$network,$context);
            }
            $source=self::tracked_source_url($payload,$network,$context);
            if ($source==='') { continue; }
            $short=self::get_or_create($source);
            if (!is_wp_error($short) && is_string($short) && $short!=='') { $links[self::normalize_network($network)]=$short; }
        }
        return ['enabled'=>true,'links'=>$links];
    }

    /** @return array<string,mixed> */
    public static function sanitize_schedule_snapshot($raw): array {
        if (!is_array($raw) || !array_key_exists('enabled',$raw)) { return []; }
        $out=['enabled'=>!empty($raw['enabled']),'links'=>[]];
        foreach (array_slice(is_array($raw['links']??null)?$raw['links']:[],0,100,true) as $network=>$url) {
            $network=self::normalize_network((string)$network);
            $url=esc_url_raw((string)$url);
            if ($network!=='' && self::is_own_short_url($url)) { $out['links'][$network]=$url; }
        }
        return $out;
    }

    /** Apply a frozen or current short link after UTM tracking and before Smart Tags/Templates. */
    public static function apply_to_payload(array $payload, string $network, array $history_context=[]): array {
        if (!empty($payload['short_link_applied'])) { return $payload; }
        $network=self::normalize_network($network);
        if ($network==='') { return $payload; }

        $snapshot=self::sanitize_schedule_snapshot($payload['short_link_snapshot']??[]);
        $frozen=$snapshot!==[];
        if ($frozen && empty($snapshot['enabled'])) { return $payload; }
        if (!$frozen && !self::is_enabled()) { return $payload; }

        $source=self::tracked_source_url($payload,$network,$history_context);
        if ($source==='') { return $payload; }
        $short='';
        if ($frozen) { $short=(string)($snapshot['links'][$network]??''); }
        if ($short==='' && !$frozen) {
            if (!empty($history_context['platform_preview'])) {
                $short=self::preview_short_url($source);
            } else {
                $created=self::get_or_create($source);
                if (!is_wp_error($created)) { $short=(string)$created; }
            }
        }
        if ($short==='' || (!$frozen && empty($history_context['platform_preview']) && !self::is_own_short_url($short))) { return $payload; }

        $original_link=trim((string)($payload['link']??''));
        $payload['short_link_applied']=1;
        $payload['short_link_network']=$network;
        $payload['short_link_post_link']=$short;
        $payload['short_link_target_url']=$source;
        if ($original_link!=='') { $payload['link']=$short; }
        return $payload;
    }

    public static function normalize_target_url(string $url): string {
        $url=trim($url);
        if ($url==='' || !wp_http_validate_url($url)) { return ''; }
        $scheme=strtolower((string)wp_parse_url($url,PHP_URL_SCHEME));
        if (!in_array($scheme,['http','https'],true)) { return ''; }
        $host=strtolower((string)wp_parse_url($url,PHP_URL_HOST));
        $site_url=home_url('/');
        $site_host=strtolower((string)wp_parse_url($site_url,PHP_URL_HOST));
        $site_scheme=strtolower((string)wp_parse_url($site_url,PHP_URL_SCHEME));
        if ($host==='' || $site_host==='' || !in_array($site_scheme,['http','https'],true)) { return ''; }
        $target_port=(int)(wp_parse_url($url,PHP_URL_PORT) ?: ($scheme==='https' ? 443 : 80));
        $site_port=(int)(wp_parse_url($site_url,PHP_URL_PORT) ?: ($site_scheme==='https' ? 443 : 80));
        $same_site=hash_equals($site_host,$host) && $scheme===$site_scheme && $target_port===$site_port;
        $allow_external=(bool)apply_filters('calliope_sp_short_links_allow_external_target',false,$url,$host,$site_host);
        if (!$same_site && !$allow_external) { return ''; }
        return esc_url_raw($url,['http','https']);
    }

    /** Ensure the target has current UTM tracking before it is shortened. */
    private static function tracked_source_url(array &$payload, string $network, array $context): string {
        $source=self::source_url($payload,$context);
        if ($source==='') { return ''; }
        if (!empty($payload['utm_tracking_post_link'])) { return $source; }
        if (class_exists('Calliope_SP_Tracking') && Calliope_SP_Tracking::is_enabled()) {
            $tracked=Calliope_SP_Tracking::track_url($source,$network);
            $tracked=self::normalize_target_url((string)$tracked);
            if ($tracked!=='') {
                $payload['utm_tracking_applied']=1;
                $payload['utm_tracking_network']=$network;
                $payload['utm_tracking_post_link']=$tracked;
                return $tracked;
            }
        }
        return $source;
    }

    private static function source_url(array $payload,array $context): string {
        foreach (['short_link_target_url','utm_tracking_post_link','link'] as $key) {
            $value=trim((string)($payload[$key]??''));
            if ($value!=='' && self::normalize_target_url($value)!=='') { return self::normalize_target_url($value); }
        }
        $post_id=absint($context['post_id']??($payload['post_id']??0));
        if ($post_id>0) {
            $permalink=get_permalink($post_id);
            if (is_string($permalink)) { return self::normalize_target_url($permalink); }
        }
        return '';
    }

    private static function short_url(string $code): string {
        $code=preg_replace('/[^A-Za-z0-9]/','',$code);
        return $code!=='' ? untrailingslashit(home_url('/')).'/sp/'.$code : '';
    }

    private static function is_own_short_url(string $url): bool {
        if ($url==='') { return false; }
        $base=preg_quote(untrailingslashit(home_url('/')).'/sp/','#');
        return (bool)preg_match('#^'.$base.'[A-Za-z0-9]{6,10}/?$#',$url);
    }

    private static function generate_code(): string {
        $raw=wp_generate_password(self::CODE_LENGTH,false,false);
        $code=preg_replace('/[^A-Za-z0-9]/','',(string)$raw);
        while (strlen($code)<self::CODE_LENGTH) { $code.=substr(hash('sha256',wp_rand().'|'.microtime(true)),0,self::CODE_LENGTH-strlen($code)); }
        return substr($code,0,self::CODE_LENGTH);
    }

    private static function find_by_code(string $code): ?array {
        global $wpdb;
        $code=preg_replace('/[^A-Za-z0-9]/','',$code);
        if (!preg_match('/^[A-Za-z0-9]{6,10}$/D',$code)) { return null; }
        Calliope_SP_Short_Link_Schema::maybe_upgrade();
        $table=Calliope_SP_Short_Link_Schema::table_name();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Public redirect lookup in plugin-owned mapping table.
        $row=$wpdb->get_row($wpdb->prepare("SELECT id,uuid,code,target_url,target_hash,status FROM {$table} WHERE code=%s LIMIT 1",$code),ARRAY_A);
        return is_array($row)?$row:null;
    }

    private static function find_by_target_hash(string $hash): ?array {
        global $wpdb;
        if (!preg_match('/^[a-f0-9]{64}$/D',$hash)) { return null; }
        Calliope_SP_Short_Link_Schema::maybe_upgrade();
        $table=Calliope_SP_Short_Link_Schema::table_name();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Exact hash lookup in plugin-owned mapping table.
        $row=$wpdb->get_row($wpdb->prepare("SELECT id,uuid,code,target_url,target_hash,status FROM {$table} WHERE target_hash=%s LIMIT 1",$hash),ARRAY_A);
        return is_array($row)?$row:null;
    }

    private static function touch(int $id): void {
        if ($id<=0) { return; }
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Lightweight operational timestamp update for plugin-owned row.
        $wpdb->update(Calliope_SP_Short_Link_Schema::table_name(),['last_used_at_utc'=>gmdate('Y-m-d H:i:s'),'updated_at_utc'=>gmdate('Y-m-d H:i:s')],['id'=>$id],['%s','%s'],['%d']);
    }

    private static function render_404(): void {
        global $wp_query;
        if ($wp_query instanceof WP_Query) { $wp_query->set_404(); }
        status_header(404);
        nocache_headers();
    }

    private static function normalize_network(string $network): string {
        $network=sanitize_key($network);
        if ($network==='bsky') return 'bluesky';
        if ($network==='webhook') return 'custom_webhook';
        return $network;
    }

    private static function uuid_fallback(): string {
        $hex=substr(hash('sha256',wp_rand().'|'.microtime(true).'|'.uniqid('',true)),0,32);
        return substr($hex,0,8).'-'.substr($hex,8,4).'-4'.substr($hex,13,3).'-a'.substr($hex,17,3).'-'.substr($hex,20,12);
    }
}
