<?php

class ScholarSync_Author_Sync {

    private $author_id;
    private $scholar_id;
    private $db_articles_checked = false;
    private $db_articles = [];
    private $articles_results = [];

    private $has_error = false;

    private $message = '';

    private $plan_limit_error = false;

    private $plan_limit_data = [];

//    const URL_CODE = 'code=UpyhlrqKwr94SE62qc5dUtWSv2aMjpJTzyM5gypdh_rsAzFu8HAYeg==&secret=78627142-8ab6-11ed-970f-000d3a4bafb4';
    /**
     * @var string - GUID from previous response, '' to start fresh
     */
    private $transaction_id;
    /**
     * @var string - transaction_id from service response; '' when sync is complete
     */
    private $next_transaction_id;

    public function __construct( $scholar_id, $transaction_id = '' ) {
        $this->scholar_id     = $scholar_id;
        $this->transaction_id = (string) $transaction_id;
    }

    public function get_scholar_id() {
        if ($this->scholar_id == NULL) {
            global $wpdb;
            $table_name = SCHOLAR_SYNC_DB_AUTHORS;
            $this->scholar_id = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->prepare('SELECT scholar_id FROM %i WHERE author_id = %d', $table_name, $this->author_id) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- %i is the WordPress identifier placeholder (WP 6.2+)
            );
        }
        return $this->scholar_id;
    }

    public function get_author_id() {
        return $this->author_id;
    }

    public function set_author_id($author_id) {
        return $this->author_id = $author_id;
    }

    public function get_articles_results() {
        return $this->articles_results;
    }

    public function author_exists() {
        if ($this->get_author_id() == NULL) {
            global $wpdb;
            $table_name = SCHOLAR_SYNC_DB_AUTHORS;
            // Normalize to the short form that insert_author() stores so the
            // lookup matches regardless of whether the caller passed the full
            // OpenAlex URL or just the bare ID.
            $lookup_id    = self::extract_id( $this->scholar_id, 'https://openalex.org/' ) ?: $this->scholar_id;
            $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->prepare( 'SELECT author_id FROM %i WHERE scholar_id = %s LIMIT 1', $table_name, $lookup_id ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- %i is the WordPress identifier placeholder (WP 6.2+)
            );
            if ($results) {
                $this->author_id = (int) $results[0]->author_id;
            }
        }
        return ($this->get_author_id()!= NULL);
    }

    private function get_article($citation_id) {
        if (!$this->db_articles_checked) {
            $this->init_db_articles();
        }

        $out = null;
        foreach ($this->db_articles as $article) {
            if ($citation_id == $article['citation_id']) {
                $out = $article;
                break;
            }
        }
        return $out;
    }

    public function add() {
        require_once SCHOLAR_SYNC_PLUGIN_DIR . '/includes/class-scholar-sync-data-source.php';
        $source = scholar_sync_get_data_source();
        $result = $source->get_author(
            $this->scholar_id,
            $this->transaction_id
        );
        if ( is_wp_error( $result ) ) {
            $this->has_error        = true;
            $this->message          = $result->get_error_message();
            $this->plan_limit_error = ( $result->get_error_code() === 'plan_limit_exceeded' );
            if ( $this->plan_limit_error ) {
                $this->plan_limit_data = (array) $result->get_error_data();
            }
        } elseif ( ! is_array( $result ) || ! isset( $result['results'] ) ) {
            $this->has_error = true;
            $this->message   = __( 'Invalid response received from the remote service. Please try again.', 'scholar-sync' );
        } else {
            $this->insert_author( $result );
            $this->insert_articles( $result );
            $this->insert_co_authors( $result );
            $this->manage_pagination( $result );
        }
    }

    private function insert_author( $data ) {
        global $wpdb;

        if ( $this->author_exists() ) {
            $this->update_author_stats( $data );
            return;
        }

        // Locate the target author in article authorships using the short OpenAlex ID.
        $scholar_short      = self::extract_id( $this->scholar_id, 'https://openalex.org/' );
        $author_name        = '';
        $author_orcid       = null;
        $author_affiliation = '';

        foreach ( $data['results'] ?? [] as $article ) {
            foreach ( $article['authorships'] ?? [] as $authorship ) {
                $a       = $authorship['author'] ?? [];
                $a_short = self::extract_id( $a['id'] ?? '', 'https://openalex.org/' );
                if ( $a_short === $scholar_short ) {
                    $author_name        = $a['display_name'] ?? '';
                    $author_orcid       = $a['orcid'] ?? null;
                    $author_affiliation = implode( '; ', $authorship['raw_affiliation_strings'] ?? [] );
                    break 2;
                }
            }
        }

        $table_name = SCHOLAR_SYNC_DB_AUTHORS;
        $values     = [
            'scholar_id'   => $scholar_short ?: $this->scholar_id,
            'orcid_id'     => self::extract_id( $author_orcid, 'https://orcid.org/' ),
            'name'         => $author_name,
            'affiliations' => $author_affiliation,
            'website'      => '',
        ];

        // The stats block (citations/h-index/i10-index/interests) only arrives on the last
        // page of a full/first_full sync. On a multi-page sync it's absent here and gets
        // backfilled by update_author_stats() once that later page arrives, since the row
        // already exists by then.
        $stats = $this->extract_stats_fields( $data );
        if ( $stats !== null ) {
            $values = array_merge( $values, $stats );
        }

        $wpdb->insert( $table_name, $values ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Insert bypasses cache safely
        $this->author_id = (int) $wpdb->insert_id;
    }

    /**
     * Extract the citations/h-index/i10-index/interests stats block from a sync-author
     * response, if present. Per the service contract these 4 fields arrive together only
     * on the last page of a full/first_full sync — never on partial syncs or intermediate
     * pages — and the fetch is best-effort, so a missing block just means "no stats update
     * this cycle," not an error.
     *
     * @param array $data Raw response from ScholarSync_Data_Source::get_author().
     * @return array{citations_all:int|null,h_index_all:int,i10_index_all:int|null,interests:string|null}|null
     */
    private function extract_stats_fields( array $data ) {
        if ( ! isset( $data['h_index'] ) ) {
            return null;
        }
        return [
            'citations_all' => isset( $data['citations_count'] ) ? (int) $data['citations_count'] : null,
            'h_index_all'   => (int) $data['h_index'],
            'i10_index_all' => isset( $data['i10_index'] ) ? (int) $data['i10_index'] : null,
            'interests'     => ( ! empty( $data['interests'] ) && is_array( $data['interests'] ) ) ? implode( ', ', $data['interests'] ) : null,
        ];
    }

    /**
     * Refresh an existing author's stats fields from a sync-author response, one field at a
     * time gated by that field's own _sync flag — a manual edit via
     * admin/actions/update_author.php flips just that field's flag to 0, and sync must then
     * leave it alone until the flag is re-enabled. Does nothing else on the author row.
     *
     * @param array $data Raw response from ScholarSync_Data_Source::get_author().
     */
    private function update_author_stats( array $data ) {
        $stats = $this->extract_stats_fields( $data );
        if ( $stats === null ) {
            return;
        }

        global $wpdb;
        $table_name = SCHOLAR_SYNC_DB_AUTHORS;

        $sync_flags = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prepare( 'SELECT citations_all_sync, h_index_all_sync, i10_index_all_sync, interests_sync FROM %i WHERE author_id = %d', $table_name, $this->author_id ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- %i is the WordPress identifier placeholder (WP 6.2+)
            ARRAY_A
        );
        if ( ! $sync_flags ) {
            return;
        }

        $field_to_flag = [
            'citations_all' => 'citations_all_sync',
            'h_index_all'   => 'h_index_all_sync',
            'i10_index_all' => 'i10_index_all_sync',
            'interests'     => 'interests_sync',
        ];

        $update = [];
        foreach ( $field_to_flag as $field => $flag ) {
            if ( (int) $sync_flags[ $flag ] === 1 ) {
                $update[ $field ] = $stats[ $field ];
            }
        }

        if ( ! empty( $update ) ) {
            $wpdb->update( $table_name, $update, [ 'author_id' => $this->author_id ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Update bypasses cache safely
        }
    }

    private function insert_articles( $data ) {
        if ( empty( $this->author_id ) ) {
            return; // Author insert failed — skip to avoid articles with author_id = 0.
        }
        global $wpdb;
        $insert_placeholders = array();
        $insert_values       = array();
        $insert_count        = 0;
        $update_queries      = array();
        $table_name          = SCHOLAR_SYNC_DB_ARTICLES;

        foreach ( $data['results'] ?? [] as $article ) {
            $title       = $article['display_name'] ?? $article['title'] ?? '';
            $year        = (int) ( $article['publication_year'] ?? 0 );
            $cited_by    = (int) ( $article['cited_by_count'] ?? 0 );
            $publication = $article['primary_location']['source']['display_name'] ?? '';
            $citation_id = md5( $title . $year );
            $all_data    = wp_json_encode( $article );
            $db_article  = $this->get_article( $citation_id );

            // Build the best available link: DOI → publisher landing page → OA URL → OpenAlex work page.
            $link = $article['doi'] ?? '';
            if ( empty( $link ) ) {
                $link = $article['primary_location']['landing_page_url'] ?? '';
            }
            if ( empty( $link ) ) {
                $link = $article['open_access']['oa_url'] ?? '';
            }
            if ( empty( $link ) ) {
                $link = $article['id'] ?? ''; // OpenAlex work page, always present.
            }

            // Build comma-separated author names from authorships.
            $author_names = array();
            foreach ( $article['authorships'] ?? [] as $authorship ) {
                $display = $authorship['author']['display_name'] ?? '';
                if ( $display !== '' ) {
                    $author_names[] = $display;
                }
            }
            $authors_str = implode( ', ', $author_names );

            if ( $db_article == null ) {
                $insert_count++;
                $insert_placeholders[] = '(%d, %s, %s, %s, %s, %s, %d, %d, %s)';
                array_push(
                    $insert_values,
                    $this->author_id,
                    $citation_id,
                    $title,
                    $link,
                    $authors_str,
                    $publication,
                    $cited_by,
                    $year,
                    $all_data
                );
            } elseif ( $db_article['is_active'] == 0 ) {
                // Soft-deleted article — leave it untouched, don't count it as synced.
                continue;
            } elseif ( $db_article['is_custom'] == 1 && ! empty( $db_article['citation_id'] ) ) {
                if ( $db_article['cited_by_value_sync'] == 1 ) {
                    $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Update bypasses cache safely
                        $table_name,
                        array( 'cited_by_value' => $cited_by ),
                        array( 'article_id' => $db_article['article_id'] )
                    );
                }
            } elseif ( $all_data != $db_article['all_data'] && $db_article['is_custom'] == 0 ) {
                $update_queries[] = array(
                    'article_id'     => $db_article['article_id'],
                    'title'          => esc_attr( $title ),
                    'link'           => $link,
                    'authors'        => $authors_str,
                    'publication'    => esc_attr( $publication ),
                    'cited_by_value' => $db_article['cited_by_value_sync'] == 1 ? $cited_by : $db_article['cited_by_value'],
                    'year'           => $year,
                    'all_data'       => $all_data,
                );
            }
        }

        if ( $insert_count > 0 ) {
            $this->articles_results['articles_created'] = $insert_count;
            $sql = 'INSERT INTO %i(author_id, citation_id, title, link, authors, publication, cited_by_value, year, all_data) VALUES '
                . implode( ', ', $insert_placeholders );
            $wpdb->query( $wpdb->prepare( $sql, $table_name, ...$insert_values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- %i for table; %d/%s placeholders for values, Insert bypasses cache safely
        }
        if ( ! empty( $update_queries ) ) {
            $this->articles_results['articles_updated'] = count( $update_queries );
            $this->update_articles_db( $update_queries );
        }
    }

    /**
     * Function to update multiple records in the database efficiently.
     *
     * @param array $records Array of records to be updated.
     * @return void
     */
    private function update_articles_db($records) {
        global $wpdb;

        if (empty($records)) {
            return;
        }
        $table_name = SCHOLAR_SYNC_DB_ARTICLES;
        // Prepare the base of the query
        $query = 'INSERT INTO %i (article_id, title, link, authors, publication, cited_by_value, year, all_data) VALUES ';
        // Prepare the values for the query
        $values = array();
        $placeholders = array();
        foreach ($records as $record) {
            $placeholders[] = "(%d, %s, %s, %s, %s, %d, %d, %s)";
            $values[] = $record['article_id'];
            $values[] = $record['title'];
            $values[] = $record['link'];
            $values[] = $record['authors'];
            $values[] = $record['publication'];
            $values[] = $record['cited_by_value'];
            $values[] = $record['year'];
            $values[] = $record['all_data'];
        }
        // Complete the query with ON DUPLICATE KEY UPDATE clause
        $query .= implode(', ', $placeholders);
        $query .= " ON DUPLICATE KEY UPDATE
                title = VALUES(title),
                link = VALUES(link),
                authors = VALUES(authors),
                publication = VALUES(publication),
                cited_by_value = VALUES(cited_by_value),
                year = VALUES(year),
                all_data = VALUES(all_data)";
        // Prepare and execute the query
        $wpdb->query($wpdb->prepare($query, array_merge(array($table_name), $values))); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- %i for table; %d/%s placeholders for values, Update bypasses cache safely
    }

    private function init_db_articles()
    {
        global $wpdb;
        $table_name = SCHOLAR_SYNC_DB_ARTICLES;
        $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Read bypasses cache safely
            $wpdb->prepare('SELECT article_id, citation_id, is_active, is_custom, all_data, cited_by_value, cited_by_value_sync FROM %i WHERE author_id = %d', $table_name, $this->author_id) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- %i is the WordPress identifier placeholder (WP 6.2+)
        );

        foreach ($results as $result) {
            $this->db_articles[] = array(
                "article_id"          => $result->article_id,
                "citation_id"         => $result->citation_id,
                "all_data"            => $result->all_data,
                "is_custom"           => (int) $result->is_custom,
                "is_active"           => (int) $result->is_active,
                "cited_by_value"      => (int) $result->cited_by_value,
                "cited_by_value_sync" => (int) $result->cited_by_value_sync,
            );
        }
        $this->db_articles_checked = true;
    }

    private function get_interests_titles($data, $delimiter = ', ') {
        // Check if "interests" key exists in the JSON
        if (isset($data['interests']) && is_array($data['interests'])) {
            // Extract titles into a new array
            $titles = array_column($data['interests'], 'title');
            return implode($delimiter, $titles);
        }
        return '';
    }

    private static function extract_id( $value, $prefix ) {
        if ( empty( $value ) ) {
            return null;
        }
        if ( strpos( $value, $prefix ) === 0 ) {
            return substr( $value, strlen( $prefix ) );
        }
        return $value;
    }

    private function get_safe_value($val_obj, $key) {
        $ret = NULL;
        if (isset($val_obj[$key])) {
            $ret = $val_obj[$key];
        }
        return $ret;
    }

    private function get_safe_from_array($obj_array, $keys_array) {
        $ret  = NULL;
        foreach ($obj_array as $obj) {
            foreach ($keys_array as $index => $key) {
                if (isset($obj[$key])) {
                    if ($index + 1 === count($keys_array)) { // last key
                        $ret = $obj[$key];
                    } else {
                        $obj = $obj[$key];
                    }
                }
            }
        }
        return $ret;
    }

    public function get_message()
    {
        return $this->message;
    }

    public function has_error()
    {
        return $this->has_error;
    }

    public function is_plan_limit_error(): bool
    {
        return $this->plan_limit_error;
    }

    public function get_plan_limit_data(): array
    {
        return $this->plan_limit_data;
    }

    private function manage_pagination( $data ) {
        $this->next_transaction_id = isset( $data['transaction_id'] ) ? (string) $data['transaction_id'] : '';
    }

    public function get_next_transaction_id(): string {
        return $this->next_transaction_id ?? '';
    }

    public function get_articles_count_message()
    {
        $count = ( $this->articles_results['articles_created'] ?? 0 ) + ( $this->articles_results['articles_updated'] ?? 0 );
        /* translators: %s: number of articles synced */
        return sprintf( __( '%s articles were successfully synced', 'scholar-sync' ), $count );
    }

    private function insert_co_authors( array $data ): void {
        $scholar_short = self::extract_id( $this->scholar_id, 'https://openalex.org/' );
        $seen          = [];
        $co_authors    = [];

        foreach ( $data['results'] ?? [] as $article ) {
            foreach ( $article['authorships'] ?? [] as $authorship ) {
                $a       = $authorship['author'] ?? [];
                $a_id    = $a['id'] ?? '';
                $a_short = self::extract_id( $a_id, 'https://openalex.org/' );
                if ( empty( $a_short ) || $a_short === $scholar_short || isset( $seen[ $a_short ] ) ) {
                    continue;
                }
                $seen[ $a_short ] = true;
                $affil_names  = array_filter( array_column( $authorship['institutions'] ?? [], 'display_name' ) );
                $co_authors[] = array(
                    'name'         => $a['display_name'] ?? '',
                    'author_id'    => $a_short,
                    'affiliations' => ! empty( $affil_names ) ? implode( ', ', $affil_names ) : null,
                );
            }
        }

        if ( empty( $co_authors ) ) { return; }

        require_once SCHOLAR_SYNC_PLUGIN_DIR . '/admin/scholar_sync_functions.php';
        global $wpdb;
        $table = SCHOLAR_SYNC_DB_CO_AUTHORS;

        foreach ( $co_authors as $co ) {
            $short_name_raw = scholar_sync_generate_short_name( $co['name'] );
            $short_name     = sanitize_text_field( $short_name_raw );
            if ( empty( $short_name ) ) { continue; }

            $scholar_id = $co['author_id'];
            $exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- %i is the WordPress identifier placeholder (WP 6.2+)
                    'SELECT co_author_id FROM %i
                     WHERE short_name = %s OR (scholar_id IS NOT NULL AND scholar_id = %s)
                     LIMIT 1',
                    $table,
                    $short_name,
                    $scholar_id
                )
            );
            if ( $exists ) { continue; }

            $wpdb->insert( $table, array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Insert bypasses cache safely
                'scholar_id'   => $scholar_id,
                'full_name'    => sanitize_text_field( $co['name'] ),
                'short_name'   => $short_name,
                'image_url'    => null,
                'profile_link' => 'https://openalex.org/' . rawurlencode( $scholar_id ),
                'affiliations' => isset( $co['affiliations'] ) ? sanitize_text_field( $co['affiliations'] ) : null,
            ) );
        }
    }

}