<?php

/**
 * SocialPromoter Poster
 *
 * Executes a queued post: retrieves the account, selects the right connector,
 * fires the API call, and updates the queue record with the result.
 *
 * @package PublisherKits_SocialPromoter
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
/**
 * Class PKITS_SP_Poster
 */
class PKITS_SP_Poster {
    /**
     * Execute a queue item: post to the platform and update the record.
     *
     * @param object $item Queue row object from the database.
     * @return bool True on success, false on failure.
     */
    /**
     * Compose the final post copy for a queue item exactly as it will be sent:
     * the stored copy with HTML entities decoded, configured hashtags appended
     * (story plus brand, per the brand-tag mode, deduped and capped per
     * platform), and the UTM-tagged link ensured present. Used by both the live
     * send path and the campaign preview so the two never drift.
     *
     * @param object $item Queue item (or item-like object) with post_id,
     *                     platform, copy_text and campaign_id.
     * @return array{copy:string,post_url:string}
     */
    public static function compose_copy( $item ) {
        $raw_url = get_permalink( (int) $item->post_id );
        // Campaign UTM tagging is premium only (PKITS_SP_Campaign is stripped from
        // the free build). compose_copy() runs on the free send path, so guard the
        // reference: free posts use the plain permalink.
        $tracked_url = ( $raw_url && class_exists( 'PKITS_SP_Campaign' ) ? PKITS_SP_Campaign::build_tracked_url_for_item( $item, $raw_url ) : '' );
        $post_url = ( $tracked_url ?: $raw_url );
        // Decode HTML entities (e.g. &#8211; from WordPress texturising titles)
        // so the live post shows real characters, not raw entity codes.
        $copy_text = html_entity_decode( (string) $item->copy_text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
        // Append configured hashtags, deduped against any already present.
        if ( (int) $item->post_id > 0 && class_exists( 'PKITS_SP_Author_Tagger' ) ) {
            $tags = PKITS_SP_Author_Tagger::get_post_hashtags( (int) $item->post_id, (string) $item->platform );
            if ( !empty( $tags ) ) {
                $present = array();
                if ( preg_match_all( '/#(\\w+)/u', $copy_text, $m ) ) {
                    foreach ( $m[1] as $h ) {
                        $present[strtolower( $h )] = true;
                    }
                }
                $add = array();
                foreach ( $tags as $slug ) {
                    if ( '' === $slug || isset( $present[strtolower( $slug )] ) ) {
                        continue;
                    }
                    $add[] = $slug;
                }
                if ( !empty( $add ) ) {
                    $copy_text = trim( $copy_text ) . ' ' . PKITS_SP_Author_Tagger::format_hashtags( $add );
                }
            }
        } elseif ( !class_exists( 'PKITS_SP_Author_Tagger' ) ) {
            // Free build: blend the standing brand hashtags directly (the premium
            // tagger above takes over when present, adding story/author tags too).
            $sp_settings = get_option( 'pkits_sp_settings', array() );
            $sp_settings = ( is_array( $sp_settings ) ? $sp_settings : array() );
            $sp_tags_mode = (string) ($sp_settings['brand_tags_mode'] ?? 'off');
            $sp_brand_raw = (string) ($sp_settings['brand_default_hashtags'] ?? '');
            // Mode semantics on the free path: 'always'/'only' both blend the brand
            // tags (there are no separately sourced story tags here, so 'only' has
            // nothing extra to exclude); 'fallback' blends only when the copy
            // carries fewer than two hashtags of its own.
            $sp_own_tags = ( preg_match_all( '/#(\\w+)/u', $copy_text, $sp_own_m ) ? count( $sp_own_m[1] ) : 0 );
            if ( 'fallback' === $sp_tags_mode && $sp_own_tags >= 2 ) {
                $sp_tags_mode = 'off';
            }
            if ( 'off' !== $sp_tags_mode && '' !== trim( $sp_brand_raw ) ) {
                $sp_caps = array(
                    'x'        => 3,
                    'linkedin' => 5,
                    'facebook' => 2,
                    'bluesky'  => 3,
                );
                $sp_cap = $sp_caps[(string) $item->platform] ?? 3;
                $sp_present = array();
                if ( preg_match_all( '/#(\\w+)/u', $copy_text, $sp_m ) ) {
                    foreach ( $sp_m[1] as $sp_h ) {
                        $sp_present[strtolower( $sp_h )] = true;
                    }
                }
                $sp_add = array();
                foreach ( explode( ',', $sp_brand_raw ) as $sp_tag ) {
                    $sp_tag = preg_replace( '/[^\\w]/u', '', $sp_tag );
                    if ( '' === $sp_tag || isset( $sp_present[strtolower( $sp_tag )] ) ) {
                        continue;
                    }
                    $sp_add[] = '#' . $sp_tag;
                    if ( count( $sp_add ) + count( $sp_present ) >= $sp_cap ) {
                        break;
                    }
                }
                if ( !empty( $sp_add ) ) {
                    $copy_text = trim( $copy_text ) . ' ' . implode( ' ', $sp_add );
                }
            }
        }
        if ( $raw_url && $tracked_url && $tracked_url !== $raw_url && false !== strpos( $copy_text, $raw_url ) ) {
            $copy_text = str_replace( $raw_url, $tracked_url, $copy_text );
        } elseif ( $post_url && false === strpos( $copy_text, $post_url ) ) {
            $copy_text = trim( $copy_text ) . ' ' . $post_url;
        }
        return array(
            'copy'     => $copy_text,
            'post_url' => $post_url,
        );
    }

    public function execute( $item ) {
        $queue = new PKITS_SP_Queue();
        // Mark as sending to prevent duplicate execution.
        $queue->update( $item->id, array(
            'status' => 'sending',
        ) );
        $account = PKITS_SP_Accounts::get( $item->account_id );
        if ( empty( $account ) ) {
            $queue->update( $item->id, array(
                'status'        => 'failed',
                'error_message' => 'Account not found: ' . $item->account_id,
                'sent_at'       => current_time( 'mysql', 1 ),
            ) );
            return false;
        }
        // Compose the final copy (UTM-tagged link plus hashtags) via the shared
        // composer so previews and live posts read identically.
        $image_data = null;
        $composed = self::compose_copy( $item );
        $copy_text = $composed['copy'];
        $post_url = $composed['post_url'];
        // Fetch image data if image_url is set on the queue item.
        if ( !empty( $item->image_url ) ) {
            // Check if image_url is a local media library attachment.
            $attachment_id = attachment_url_to_postid( $item->image_url );
            if ( $attachment_id ) {
                $image_data = PKITS_SP_Image_Handler::get_attachment_data( $attachment_id );
            } else {
                $image_data = PKITS_SP_Image_Handler::get_image_data_from_url( $item->image_url );
                if ( is_wp_error( $image_data ) ) {
                    $image_data = null;
                }
            }
        }
        $connector = $this->get_connector( $item->platform );
        if ( null === $connector ) {
            $queue->update( $item->id, array(
                'status'        => 'failed',
                'error_message' => 'No connector available for platform: ' . $item->platform,
                'sent_at'       => current_time( 'mysql', 1 ),
            ) );
            return false;
        }
        // Execute the post via registry - no platform-specific switch needed.
        $result = $connector->post(
            $account,
            $copy_text,
            $post_url,
            $image_data
        );
        if ( is_wp_error( $result ) ) {
            $queue->update( $item->id, array(
                'status'        => 'failed',
                'error_message' => $result->get_error_message(),
                'sent_at'       => current_time( 'mysql', 1 ),
            ) );
            return false;
        }
        // Record any non-fatal degradation so an unbranded / imageless post is
        // diagnosable from the history screen rather than a silent mystery.
        $notes = array();
        if ( isset( $result['_image_error'] ) ) {
            $notes[] = 'Posted without image: ' . $result['_image_error'];
        }
        $queue->update( $item->id, array(
            'status'           => 'sent',
            'platform_post_id' => $result['platform_post_id'] ?? '',
            'error_message'    => implode( ' | ', $notes ),
            'sent_at'          => current_time( 'mysql', 1 ),
        ) );
        return true;
    }

    /**
     * Get the connector instance for a given platform from the registry.
     *
     * @param string $platform Platform slug.
     * @return PKITS_SP_Connector_Interface|null
     */
    public function get_connector( $platform ) {
        return PKITS_SP_Platform_Registry::get( $platform );
    }

}
