<?php

/**
 * Base Importer Class
 *
 * @package ProRank\SEO\Core\Importers
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Core\Importers;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}


/**
 * Abstract BaseImporter class
 * 
 * Base class for all competitor importers
 */


abstract class BaseImporter {
    
    /**
     * Import statistics
     *
     * @var array
     */
    protected array $stats = [
        'posts' => 0,
        'pages' => 0,
        'terms' => 0,
        'locations' => 0,
        'redirects' => 0,
        'settings' => 0,
        'errors' => [],
    ];
    
    /**
     * Import types to process
     *
     * @var array
     */
    protected array $import_types = ['meta', 'settings', 'redirects', 'schema', 'locations'];
    
    /**
     * Constructor
     *
     * @param array $types Import types to process
     */
    public function __construct(array $types = []) {
        if (!empty($types)) {
            $this->import_types = $types;
        }
    }
    
    /**
     * Run the import
     *
     * @return array Import results
     */
    abstract public function import(): array;
    
    /**
     * Check if the competitor plugin is installed
     *
     * @return bool
     */
    abstract public function is_available(): bool;
    
    /**
     * Import post meta data
     *
     * @return void
     */
    protected function import_post_meta(): void {
        if (!in_array('meta', $this->import_types, true)) {
            return;
        }
        
        $post_types = get_post_types(['public' => true]);
        
        foreach ($post_types as $post_type) {
            $posts = get_posts([
                'post_type' => $post_type,
                'posts_per_page' => -1,
                'post_status' => 'any',
            ]);
            
            foreach ($posts as $post) {
                $this->import_single_post_meta($post->ID);
                if ($post->post_type === 'page') {
                    $this->stats['pages']++;
                } else {
                    $this->stats['posts']++;
                }
            }
        }
    }
    
    /**
     * Import single post meta
     *
     * @param int $post_id Post ID
     * @return void
     */
    abstract protected function import_single_post_meta(int $post_id): void;
    
    /**
     * Import term meta data
     *
     * @return void
     */
    protected function import_term_meta(): void {
        if (!in_array('meta', $this->import_types, true)) {
            return;
        }
        
        $taxonomies = get_taxonomies(['public' => true]);
        
        foreach ($taxonomies as $taxonomy) {
            $terms = get_terms([
                'taxonomy' => $taxonomy,
                'hide_empty' => false,
            ]);
            
            if (is_wp_error($terms)) {
                continue;
            }
            
            foreach ($terms as $term) {
                $this->import_single_term_meta($term->term_id);
                $this->stats['terms']++;
            }
        }
    }
    
    /**
     * Import single term meta
     *
     * @param int $term_id Term ID
     * @return void
     */
    abstract protected function import_single_term_meta(int $term_id): void;
    
    /**
     * Map competitor meta key to ProRank meta key
     *
     * @param string $competitor_key Competitor meta key
     * @return string ProRank meta key
     */
    abstract protected function map_meta_key(string $competitor_key): string;
    
    /**
     * Import settings
     *
     * @return void
     */
    protected function import_settings(): void {
        if (!in_array('settings', $this->import_types, true)) {
            return;
        }
        
        // Override in child classes
    }
    
    /**
     * Import redirects
     *
     * @return void
     */
    protected function import_redirects(): void {
        if (!in_array('redirects', $this->import_types, true)) {
            return;
        }
        
        // Override in child classes
    }
    
    /**
     * Import schema settings
     *
     * @return void
     */
    protected function import_schema(): void {
        if (!in_array('schema', $this->import_types, true)) {
            return;
        }
        
        // Override in child classes
    }

    /**
     * Import local SEO locations
     *
     * @return void
     */
    protected function import_locations(): void {
        if (!in_array('locations', $this->import_types, true)) {
            return;
        }

        // Override in child classes
    }
    
    /**
     * Get import statistics
     *
     * @return array
     */
    public function get_stats(): array {
        return $this->stats;
    }
    
    /**
     * Log import error
     *
     * @param string $message Error message
     * @param array  $context Additional context
     * @return void
     */
    protected function log_error(string $message, array $context = []): void {
        $this->stats['errors'][] = [
            'message' => $message,
            'context' => $context,
            'timestamp' => current_time('mysql'),
        ];
        
        prorank_log('ProRank SEO Import Error: ' . $message . ' ' . wp_json_encode($context));
    }
    
    /**
     * Clean and sanitize meta value
     *
     * @param mixed $value Meta value
     * @return mixed Sanitized value
     */
    protected function sanitize_meta_value($value) {
        if (is_string($value)) {
            return sanitize_text_field($value);
        }
        
        if (is_array($value)) {
            return array_map([$this, 'sanitize_meta_value'], $value);
        }
        
        return $value;
    }

    /**
     * Merge array data into an option without removing existing keys.
     *
     * @param string $option_key Option key.
     * @param array  $incoming   Incoming settings.
     * @return void
     */
    protected function merge_option_array(string $option_key, array $incoming): void {
        if (empty($incoming)) {
            return;
        }

        $existing = get_option($option_key, []);
        if (!is_array($existing)) {
            $existing = [];
        }

        update_option($option_key, array_merge($existing, $incoming));
    }

    /**
     * Merge imported values into canonical Site Basics settings.
     *
     * Accepts legacy aliases used by importers/wizard payloads and normalizes them
     * to the keys used by the Site Basics UI/API.
     *
     * @param array $incoming Imported settings payload.
     * @return void
     */
    protected function merge_site_basics(array $incoming): void {
        if (empty($incoming)) {
            return;
        }

        $aliases = [
            'site_representation' => 'org_or_person',
            'organization_name' => 'org_name',
            'organization_logo' => 'org_logo',
        ];

        $normalized = [];
        foreach ($incoming as $key => $value) {
            $target = $aliases[$key] ?? $key;

            if ($target === 'org_or_person') {
                $raw = strtolower((string) $value);
                if (in_array($raw, ['company', 'organization', 'org', 'business'], true)) {
                    $value = 'organization';
                } elseif (in_array($raw, ['person', 'individual'], true)) {
                    $value = 'person';
                } else {
                    continue;
                }
            }

            if ($target === 'twitter_username') {
                $value = ltrim(trim((string) $value), '@');
            }

            if ($this->is_empty_site_basics_value($value)) {
                continue;
            }

            $normalized[$target] = $value;
        }

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

        $existing = get_option('prorank_seo_site_basics', []);
        if (!is_array($existing)) {
            $existing = [];
        }

        update_option('prorank_seo_site_basics', array_merge($existing, $normalized));
    }

    /**
     * Check if a Site Basics value should be skipped when importing.
     *
     * @param mixed $value Value to inspect.
     * @return bool
     */
    private function is_empty_site_basics_value($value): bool {
        if ($value === null) {
            return true;
        }

        if (is_string($value)) {
            return trim($value) === '';
        }

        if (is_array($value)) {
            return empty($value);
        }

        return false;
    }

    /**
     * Save an imported redirect into ProRank redirect storage.
     *
     * @param array $redirect Redirect data.
     * @return bool True when saved, false on validation/storage failure.
     */
    protected function save_redirect(array $redirect): bool {
        global $wpdb;

        $source_raw = isset($redirect['from']) ? (string) $redirect['from'] : '';
        $target_raw = isset($redirect['to']) ? (string) $redirect['to'] : '';
        $type_raw = isset($redirect['type']) ? (int) $redirect['type'] : 301;
        $is_regex = !empty($redirect['is_regex']) ? 1 : 0;
        $status = isset($redirect['status']) ? sanitize_key((string) $redirect['status']) : 'active';
        $notes = isset($redirect['notes'])
            ? sanitize_textarea_field((string) $redirect['notes'])
            : 'Imported from competitor plugin';

        if (!in_array($status, ['active', 'inactive'], true)) {
            $status = 'active';
        }

        $source = $this->normalize_redirect_source($source_raw, (bool) $is_regex);
        $target = $this->normalize_redirect_target($target_raw);
        $type = $this->normalize_redirect_type($type_raw);

        if ($source === '' || $target === '') {
            return false;
        }

        $table = $wpdb->prefix . 'prorank_redirects';
        if ($this->ensure_redirect_table()) {
            $source_candidates = $this->build_redirect_source_lookup_candidates($source, (bool) $is_regex);
            $placeholders = implode(', ', array_fill(0, count($source_candidates), '%s'));
            $existing_id = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT id FROM {$table} WHERE source_url IN ({$placeholders}) LIMIT 1",
                    ...$source_candidates
                )
            );

            $data = [
                'source_url' => $source,
                'target_url' => $target,
                'type' => $type,
                'is_regex' => $is_regex,
                'status' => $status,
                'priority' => 0,
                'notes' => $notes,
                'created_by' => (int) (get_current_user_id() ?: 0),
            ];

            $format = ['%s', '%s', '%d', '%d', '%s', '%d', '%s', '%d'];

            if ($existing_id) {
                $updated = $wpdb->update(
                    $table,
                    $data,
                    ['id' => (int) $existing_id],
                    $format,
                    ['%d']
                );

                return $updated !== false;
            }

            $inserted = $wpdb->insert($table, $data, $format);
            return $inserted !== false;
        }

        // Fallback for unexpected environments where redirects table is unavailable.
        $redirects = get_option('prorank_seo_redirects', []);
        if (!is_array($redirects)) {
            $redirects = [];
        }

        foreach ($redirects as $existing) {
            if (
                isset($existing['from'], $existing['to'], $existing['type']) &&
                (string) $existing['from'] === $source &&
                (string) $existing['to'] === $target &&
                (int) $existing['type'] === $type
            ) {
                return true;
            }
        }

        $redirects[] = [
            'from' => $source,
            'to' => $target,
            'type' => $type,
            'is_regex' => $is_regex,
            'status' => $status,
            'created' => current_time('mysql'),
            'notes' => $notes,
        ];
        update_option('prorank_seo_redirects', $redirects);

        return true;
    }

    /**
     * Ensure redirect storage table exists.
     *
     * @return bool
     */
    protected function ensure_redirect_table(): bool {
        global $wpdb;

        $table = $wpdb->prefix . 'prorank_redirects';
        if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)) === $table) {
            return true;
        }

        if (!class_exists('\\ProRank\\SEO\\Database\\RedirectsTableInstaller')) {
            $plugin_dir = defined('PRORANK_SEO_PLUGIN_DIR')
                ? PRORANK_SEO_PLUGIN_DIR
                : (defined('PRORANK_PLUGIN_DIR') ? PRORANK_PLUGIN_DIR : '');
            if ($plugin_dir !== '') {
                $installer = $plugin_dir . 'includes/Database/RedirectsTableInstaller.php';
                if (file_exists($installer)) {
                    require_once $installer;
                }
            }
        }

        if (class_exists('\\ProRank\\SEO\\Database\\RedirectsTableInstaller')) {
            \ProRank\SEO\Database\RedirectsTableInstaller::install();
        }

        return $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)) === $table;
    }

    /**
     * Normalize redirect source (relative path or regex).
     *
     * @param string $source   Source URL or pattern.
     * @param bool   $is_regex Whether source is regex.
     * @return string
     */
    protected function normalize_redirect_source(string $source, bool $is_regex = false): string {
        $source = trim(wp_unslash($source));
        if ($source === '') {
            return '';
        }

        if ($is_regex) {
            return sanitize_text_field($source);
        }

        if (preg_match('#^https?://#i', $source)) {
            $parts = wp_parse_url($source);
            if (is_array($parts)) {
                $path = isset($parts['path']) ? (string) $parts['path'] : '/';
                if (!empty($parts['query'])) {
                    $path .= '?' . $parts['query'];
                }
                $source = $path;
            }
        }

        if ($source === '') {
            return '';
        }

        if ($source[0] !== '/') {
            $source = '/' . ltrim($source, '/');
        }

        $parts = explode('?', $source, 2);
        $path = $parts[0] ?? '';
        $query = $parts[1] ?? '';

        if ($path !== '/') {
            $path = untrailingslashit($path);
            if ($path === '') {
                $path = '/';
            }
        }

        $normalized = $query !== '' ? $path . '?' . $query : $path;

        return sanitize_text_field($normalized);
    }

    /**
     * Build exact-match lookup candidates for stored redirect sources.
     *
     * @param string $source   Normalized source URL or pattern.
     * @param bool   $is_regex Whether the source is regex.
     * @return array<int, string>
     */
    protected function build_redirect_source_lookup_candidates(string $source, bool $is_regex = false): array {
        if ($is_regex || $source === '') {
            return [$source];
        }

        $candidates = [$source];
        $parts = explode('?', $source, 2);
        $path = $parts[0] ?? '';
        $query = $parts[1] ?? '';

        if ($path !== '' && $path !== '/') {
            $trailed = trailingslashit(untrailingslashit($path));
            $candidate = $query !== '' ? $trailed . '?' . $query : $trailed;
            if (!in_array($candidate, $candidates, true)) {
                $candidates[] = $candidate;
            }
        }

        return $candidates;
    }

    /**
     * Normalize redirect target URL/path.
     *
     * @param string $target Target URL or path.
     * @return string
     */
    protected function normalize_redirect_target(string $target): string {
        $target = trim(wp_unslash($target));
        if ($target === '') {
            return '';
        }

        $sanitized_url = esc_url_raw($target);
        if ($sanitized_url !== '') {
            return $sanitized_url;
        }

        if ($target[0] !== '/') {
            $target = '/' . ltrim($target, '/');
        }

        return sanitize_text_field($target);
    }

    /**
     * Normalize HTTP redirect status code.
     *
     * @param int $type Status code.
     * @return int
     */
    protected function normalize_redirect_type(int $type): int {
        $allowed = [301, 302, 303, 307, 308, 410];
        if (!in_array($type, $allowed, true)) {
            return 301;
        }

        return $type;
    }

    /**
     * Save an imported location to Local SEO option storage.
     *
     * @param array $location Raw location data.
     * @return bool True when saved.
     */
    protected function save_location(array $location): bool {
        $sanitized = $this->sanitize_location_payload($location);
        if (empty($sanitized['name'])) {
            return false;
        }

        $locations = $this->get_locations_option_normalized();
        $matching_id = $this->find_matching_location_id($locations, $sanitized);

        if ($matching_id !== null && isset($locations[$matching_id])) {
            $existing = is_array($locations[$matching_id]) ? $locations[$matching_id] : [];
            $sanitized['created_at'] = isset($existing['created_at']) ? (string) $existing['created_at'] : current_time('mysql');
            $locations[$matching_id] = array_merge($existing, $sanitized, [
                'updated_at' => current_time('mysql'),
            ]);
            update_option('prorank_local_seo_locations', $locations);

            return true;
        }

        if ($this->has_primary_location($locations) === false && empty($sanitized['isPrimary'])) {
            $sanitized['isPrimary'] = true;
        }

        $location_id = isset($location['id']) && is_string($location['id']) ? sanitize_key($location['id']) : '';
        if ($location_id === '' || isset($locations[$location_id])) {
            $location_id = 'loc_' . wp_generate_uuid4();
        }

        $locations[$location_id] = $sanitized;
        update_option('prorank_local_seo_locations', $locations);

        return true;
    }

    /**
     * Normalize existing locations option into id => location shape.
     *
     * @return array<string, array>
     */
    protected function get_locations_option_normalized(): array {
        $stored = get_option('prorank_local_seo_locations', []);
        if (!is_array($stored)) {
            return [];
        }

        $normalized = [];
        foreach ($stored as $key => $location) {
            if (!is_array($location)) {
                continue;
            }

            $id = '';
            if (is_string($key) && $key !== '' && !is_numeric($key)) {
                $id = sanitize_key($key);
            } elseif (!empty($location['id']) && is_string($location['id'])) {
                $id = sanitize_key($location['id']);
            }

            if ($id === '' || isset($normalized[$id])) {
                $id = 'loc_' . wp_generate_uuid4();
            }

            $normalized[$id] = $location;
        }

        return $normalized;
    }

    /**
     * Sanitize location payload to Local SEO option shape.
     *
     * @param array $location Raw location data.
     * @return array
     */
    protected function sanitize_location_payload(array $location): array {
        $opening_hours = [];
        if (isset($location['openingHours']) && is_array($location['openingHours'])) {
            $opening_hours = $location['openingHours'];
        } elseif (isset($location['hours']) && is_array($location['hours'])) {
            $opening_hours = $location['hours'];
        }

        return [
            'name' => sanitize_text_field((string) ($location['name'] ?? '')),
            'description' => sanitize_textarea_field((string) ($location['description'] ?? '')),
            'address' => sanitize_text_field((string) ($location['address'] ?? '')),
            'city' => sanitize_text_field((string) ($location['city'] ?? '')),
            'state' => sanitize_text_field((string) ($location['state'] ?? '')),
            'postalCode' => sanitize_text_field((string) ($location['postalCode'] ?? '')),
            'country' => sanitize_text_field((string) ($location['country'] ?? '')),
            'phone' => sanitize_text_field((string) ($location['phone'] ?? '')),
            'email' => sanitize_email((string) ($location['email'] ?? '')),
            'latitude' => (float) ($location['latitude'] ?? 0),
            'longitude' => (float) ($location['longitude'] ?? 0),
            'status' => (isset($location['status']) && $location['status'] === 'inactive') ? 'inactive' : 'active',
            'isPrimary' => !empty($location['isPrimary']),
            'openingHours' => $this->normalize_opening_hours($opening_hours),
            'images' => array_map('esc_url_raw', isset($location['images']) && is_array($location['images']) ? $location['images'] : []),
            'created_at' => !empty($location['created_at']) ? sanitize_text_field((string) $location['created_at']) : current_time('mysql'),
            'updated_at' => current_time('mysql'),
        ];
    }

    /**
     * Normalize opening hours payload.
     *
     * @param array $hours Raw opening hours.
     * @return array
     */
    protected function normalize_opening_hours(array $hours): array {
        $normalized = [];

        foreach ($hours as $day => $entry) {
            $day_key = sanitize_key((string) $day);
            if ($day_key === '') {
                continue;
            }

            if (is_string($entry)) {
                $normalized[$day_key] = sanitize_text_field($entry);
                continue;
            }

            if (!is_array($entry)) {
                continue;
            }

            $normalized[$day_key] = [
                'open' => sanitize_text_field((string) ($entry['open'] ?? '')),
                'close' => sanitize_text_field((string) ($entry['close'] ?? '')),
                'closed' => !empty($entry['closed']),
            ];
        }

        return $normalized;
    }

    /**
     * Find an existing location id that matches a location payload.
     *
     * @param array<string, array> $locations Existing locations.
     * @param array                $candidate Candidate location payload.
     * @return string|null
     */
    protected function find_matching_location_id(array $locations, array $candidate): ?string {
        $name = strtolower(trim((string) ($candidate['name'] ?? '')));
        $address = strtolower(trim((string) ($candidate['address'] ?? '')));
        $city = strtolower(trim((string) ($candidate['city'] ?? '')));
        $postal = strtolower(trim((string) ($candidate['postalCode'] ?? '')));

        foreach ($locations as $id => $location) {
            if (!is_array($location)) {
                continue;
            }

            $existing_name = strtolower(trim((string) ($location['name'] ?? '')));
            $existing_address = strtolower(trim((string) ($location['address'] ?? '')));
            $existing_city = strtolower(trim((string) ($location['city'] ?? '')));
            $existing_postal = strtolower(trim((string) ($location['postalCode'] ?? '')));

            if ($name === '' || $existing_name === '' || $name !== $existing_name) {
                continue;
            }

            if ($address !== '' && $existing_address !== '' && $address !== $existing_address) {
                continue;
            }

            if ($city !== '' && $existing_city !== '' && $city !== $existing_city) {
                continue;
            }

            if ($postal !== '' && $existing_postal !== '' && $postal !== $existing_postal) {
                continue;
            }

            return $id;
        }

        return null;
    }

    /**
     * Determine whether current location data already has a primary location.
     *
     * @param array<string, array> $locations Existing locations.
     * @return bool
     */
    protected function has_primary_location(array $locations): bool {
        foreach ($locations as $location) {
            if (is_array($location) && !empty($location['isPrimary'])) {
                return true;
            }
        }

        return false;
    }
}
