<?php

namespace WPSN\Modules\BulkTable\Admin;

use WPSN\Core\Container;
use WPSN\SEO\MetaKeys;

defined('ABSPATH') || exit;

final class TermBulkTableImporter
{
    /**
     * Internal marker used in ops/preview to represent an explicit "clear".
     */
    public const CLEAR_MARKER = '__WPSN_CLEAR__';

    private Container $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    /**
     * @param array{apply_only_changed?:bool,empty_cells?:string,clear_token?:string,mapping?:string} $options
     * @return array{errors:array<int,string>,changes:array<int,array<string,mixed>>,ops:array<int,array<string,mixed>>,total_rows:int,total_changes:int}
     */
    public function parseAndDiff(string $filePath, array $options = []): array
    {
        $errors = [];

        $applyOnlyChanged = !isset($options['apply_only_changed']) || (bool) $options['apply_only_changed'];
        $emptyCells = isset($options['empty_cells']) ? (string) $options['empty_cells'] : 'ignore';
        if (!in_array($emptyCells, ['ignore', 'clear'], true)) {
            $emptyCells = 'ignore';
        }

        $clearToken = isset($options['clear_token']) ? trim((string) $options['clear_token']) : '__CLEAR__';
        if ($clearToken === '') {
            $clearToken = '__CLEAR__';
        }

        $mapping = isset($options['mapping']) ? sanitize_key((string) $options['mapping']) : 'auto';
        if (!in_array($mapping, ['auto', 'id_only'], true)) {
            $mapping = 'auto';
        }

        $rows = $this->readCsv($filePath, $errors);
        if (empty($rows)) {
            return [
                'errors' => $errors,
                'changes' => [],
                'ops' => [],
                'total_rows' => 0,
                'total_changes' => 0,
            ];
        }

        $header = array_shift($rows);
        if (!is_array($header)) {
            $errors[] = __('Invalid CSV header.', 'wp-seo-next');
            return [
                'errors' => $errors,
                'changes' => [],
                'ops' => [],
                'total_rows' => 0,
                'total_changes' => 0,
            ];
        }

        $map = $this->mapHeader($header);
        $hasTax = isset($map['taxonomy']);
        $hasId = isset($map['term_id']);
        $hasSlug = isset($map['term_slug']);

        if (!$hasTax) {
            $errors[] = __('CSV must include taxonomy column.', 'wp-seo-next');
        }
        if ($mapping === 'id_only' && !$hasId) {
            $errors[] = __('CSV must include term_id column (mapping = ID only).', 'wp-seo-next');
        }
        if ($mapping !== 'id_only' && !$hasId && !$hasSlug) {
            $errors[] = __('CSV must include term_id or term_slug column for mapping.', 'wp-seo-next');
        }

        $changes = [];
        $ops = [];
        $totalChanges = 0;

        foreach ($rows as $i => $row) {
            if (!is_array($row) || empty(array_filter($row, static fn($v) => trim((string) $v) !== ''))) {
                continue;
            }

            $taxonomy = isset($map['taxonomy']) ? sanitize_key((string) ($row[$map['taxonomy']] ?? '')) : '';
            if ($taxonomy === '' || !taxonomy_exists($taxonomy)) {
                $errors[] = sprintf(__('Row %d: invalid taxonomy.', 'wp-seo-next'), $i + 2);
                continue;
            }

            $resolved = $this->resolveTermFromRow($row, $map, $taxonomy, $mapping);
            $termId = $resolved['term_id'];
            $mappedBy = $resolved['mapped_by'];
            $term = $resolved['term'];

            if ($termId <= 0 || !$term) {
                $errors[] = sprintf(__('Row %d: could not map row to a term (id/slug).', 'wp-seo-next'), $i + 2);
                continue;
            }

            if (!$this->canEditTerm($termId)) {
                $errors[] = sprintf(__('Row %d: no permission to edit term #%d.', 'wp-seo-next'), $i + 2, $termId);
                continue;
            }

            $fields = $this->extractFieldsFromRow($row, $map, $emptyCells, $clearToken);
            if (empty($fields)) {
                continue;
            }

            $changedFields = [];
            foreach ($fields as $field => $newValue) {
                $old = $this->getCurrentValue($termId, $field);
                $compareNew = ($newValue === self::CLEAR_MARKER) ? '' : (string) $newValue;
                if ($applyOnlyChanged && trim($old) === trim($compareNew)) {
                    continue;
                }
                $changedFields[$field] = $newValue;

                $changes[] = [
                    'term_id' => $termId,
                    'taxonomy' => $taxonomy,
                    'term_name' => is_object($term) ? (string) ($term->name ?? '') : '',
                    'mapped_by' => $mappedBy,
                    'field' => $field,
                    'old' => $old,
                    'new' => ($newValue === self::CLEAR_MARKER) ? __('(clear)', 'wp-seo-next') : (string) $newValue,
                ];
                $totalChanges++;
            }

            if (!empty($changedFields)) {
                $ops[] = [
                    'term_id' => $termId,
                    'taxonomy' => $taxonomy,
                    'fields' => $changedFields,
                ];
            }
        }

        return [
            'errors' => $errors,
            'changes' => $changes,
            'ops' => $ops,
            'total_rows' => count($rows),
            'total_changes' => $totalChanges,
        ];
    }

    /**
     * Resolve a row to a term ID (supports staging→production imports via taxonomy+slug).
     *
     * @param array<int,string> $row
     * @param array<string,int> $map
     * @return array{term_id:int,mapped_by:string,term:?\WP_Term}
     */
    private function resolveTermFromRow(array $row, array $map, string $taxonomy, string $mapping): array
    {
        // 1) Try term_id.
        if (isset($map['term_id'])) {
            $tid = (int) ($row[$map['term_id']] ?? 0);
            if ($tid > 0) {
                $maybe = get_term($tid, $taxonomy);
                if ($maybe && !is_wp_error($maybe)) {
                    return [
                        'term_id' => (int) $tid,
                        'mapped_by' => 'id',
                        'term' => $maybe instanceof \WP_Term ? $maybe : null,
                    ];
                }
            }
        }

        if ($mapping === 'id_only') {
            return [
                'term_id' => 0,
                'mapped_by' => 'id',
                'term' => null,
            ];
        }

        // 2) Try slug.
        if (isset($map['term_slug'])) {
            $slug = sanitize_title((string) ($row[$map['term_slug']] ?? ''));
            if ($slug !== '') {
                $maybe = get_term_by('slug', $slug, $taxonomy);
                if ($maybe && !is_wp_error($maybe)) {
                    return [
                        'term_id' => (int) ($maybe->term_id ?? 0),
                        'mapped_by' => 'slug',
                        'term' => $maybe instanceof \WP_Term ? $maybe : null,
                    ];
                }
            }
        }

        return [
            'term_id' => 0,
            'mapped_by' => 'slug',
            'term' => null,
        ];
    }

    /**
     * @param array<int,array<string,mixed>> $ops
     */
    public function applyOps(array $ops): int
    {
        $count = 0;
        foreach ($ops as $op) {
            $termId = isset($op['term_id']) ? (int) $op['term_id'] : 0;
            $taxonomy = isset($op['taxonomy']) ? sanitize_key((string) $op['taxonomy']) : '';
            $fields = isset($op['fields']) && is_array($op['fields']) ? $op['fields'] : [];
            if ($termId <= 0 || $taxonomy === '' || empty($fields) || !taxonomy_exists($taxonomy)) {
                continue;
            }
            if (!$this->canEditTerm($termId)) {
                continue;
            }
            $term = get_term($termId, $taxonomy);
            if (!$term || is_wp_error($term)) {
                continue;
            }

            $count += $this->applyToTerm($termId, $fields);

            // Bust caches.
            $this->container->get('events')->dispatch('seo/meta/updated', [$termId, 'term']);
        }
        return $count;
    }

    private function canEditTerm(int $termId): bool
    {
        // WP-CLI is an admin context by nature; avoid blocking automation pipelines.
        if (defined('WP_CLI') && WP_CLI) {
            return true;
        }

        return current_user_can('edit_term', $termId);
    }

    /**
     * @param array<string,mixed> $fields
     */
    private function applyToTerm(int $termId, array $fields): int
    {
        $count = 0;

        foreach ($fields as $field => $value) {
            $value = (string) $value;
            if ($value === self::CLEAR_MARKER) {
                $value = '';
            }
            $value = trim($value);

            switch ($field) {
                case 'seo_title':
                    $count += $this->updateOrDeleteTermMeta($termId, MetaKeys::TITLE, sanitize_text_field($value));
                    break;
                case 'meta_description':
                    $count += $this->updateOrDeleteTermMeta($termId, MetaKeys::DESCRIPTION, sanitize_textarea_field($value));
                    break;
                case 'canonical':
                    $count += $this->updateOrDeleteTermMeta($termId, MetaKeys::CANONICAL, $this->sanitizeUrlAllowRelative($value));
                    break;
                case 'robots_index':
                    $v = $this->sanitizeRobotsIndex($value);
                    if ($v !== '' && $v !== 'default') {
                        update_term_meta($termId, MetaKeys::ROBOTS_INDEX, $v);
                        $count++;
                    } else {
                        if (delete_term_meta($termId, MetaKeys::ROBOTS_INDEX)) {
                            $count++;
                        }
                    }
                    break;
                case 'robots_follow':
                    $v = $this->sanitizeRobotsFollow($value);
                    if ($v !== '' && $v !== 'default') {
                        update_term_meta($termId, MetaKeys::ROBOTS_FOLLOW, $v);
                        $count++;
                    } else {
                        if (delete_term_meta($termId, MetaKeys::ROBOTS_FOLLOW)) {
                            $count++;
                        }
                    }
                    break;
                case 'og_title':
                    $count += $this->updateOrDeleteTermMeta($termId, MetaKeys::OG_TITLE, sanitize_text_field($value));
                    break;
                case 'og_description':
                    $count += $this->updateOrDeleteTermMeta($termId, MetaKeys::OG_DESCRIPTION, sanitize_textarea_field($value));
                    break;
                case 'og_image':
                    $count += $this->updateOrDeleteTermMeta($termId, MetaKeys::OG_IMAGE, $this->sanitizeUrlAllowRelative($value));
                    break;
                case 'twitter_title':
                    $count += $this->updateOrDeleteTermMeta($termId, MetaKeys::TWITTER_TITLE, sanitize_text_field($value));
                    break;
                case 'twitter_description':
                    $count += $this->updateOrDeleteTermMeta($termId, MetaKeys::TWITTER_DESCRIPTION, sanitize_textarea_field($value));
                    break;
                case 'twitter_image':
                    $count += $this->updateOrDeleteTermMeta($termId, MetaKeys::TWITTER_IMAGE, $this->sanitizeUrlAllowRelative($value));
                    break;
                default:
                    break;
            }
        }

        return $count;
    }

    private function updateOrDeleteTermMeta(int $termId, string $metaKey, string $value): int
    {
        $value = trim($value);
        if ($value !== '') {
            update_term_meta($termId, $metaKey, $value);
            return 1;
        }
        return delete_term_meta($termId, $metaKey) ? 1 : 0;
    }

    private function getCurrentValue(int $termId, string $field): string
    {
        switch ($field) {
            case 'seo_title':
                return (string) get_term_meta($termId, MetaKeys::TITLE, true);
            case 'meta_description':
                return (string) get_term_meta($termId, MetaKeys::DESCRIPTION, true);
            case 'canonical':
                return (string) get_term_meta($termId, MetaKeys::CANONICAL, true);
            case 'robots_index':
                return (string) get_term_meta($termId, MetaKeys::ROBOTS_INDEX, true);
            case 'robots_follow':
                return (string) get_term_meta($termId, MetaKeys::ROBOTS_FOLLOW, true);
            case 'og_title':
                return (string) get_term_meta($termId, MetaKeys::OG_TITLE, true);
            case 'og_description':
                return (string) get_term_meta($termId, MetaKeys::OG_DESCRIPTION, true);
            case 'og_image':
                return (string) get_term_meta($termId, MetaKeys::OG_IMAGE, true);
            case 'twitter_title':
                return (string) get_term_meta($termId, MetaKeys::TWITTER_TITLE, true);
            case 'twitter_description':
                return (string) get_term_meta($termId, MetaKeys::TWITTER_DESCRIPTION, true);
            case 'twitter_image':
                return (string) get_term_meta($termId, MetaKeys::TWITTER_IMAGE, true);
            default:
                return '';
        }
    }

    /**
     * @param array<int,string> $header
     * @return array<string,int>
     */
    private function mapHeader(array $header): array
    {
        $map = [];
        $aliases = [
            'term_id' => ['term_id', 'id', 'termid'],
            'taxonomy' => ['taxonomy', 'tax'],
            'term_slug' => ['term_slug', 'slug', 'termslug'],
            'seo_title' => ['seo_title', 'title', 'seoTitle'],
            'meta_description' => ['meta_description', 'description', 'meta_description_override', 'metaDescription'],
            'canonical' => ['canonical', 'canon'],
            'robots_index' => ['robots_index', 'robotsindex', 'noindex'],
            'robots_follow' => ['robots_follow', 'robotsfollow', 'nofollow'],
            'og_title' => ['og_title', 'ogtitle'],
            'og_description' => ['og_description', 'ogdesc', 'og_description_override'],
            'og_image' => ['og_image', 'ogimage'],
            'twitter_title' => ['twitter_title', 'twittertitle', 'tw_title', 'twtitle'],
            'twitter_description' => ['twitter_description', 'twitterdesc', 'tw_description', 'twdesc'],
            'twitter_image' => ['twitter_image', 'twitterimage', 'tw_image', 'twimage'],
        ];

        foreach ($header as $idx => $col) {
            $key = sanitize_key((string) $col);
            foreach ($aliases as $field => $list) {
                if (in_array($key, $list, true)) {
                    $map[$field] = (int) $idx;
                    break;
                }
            }
        }

        return $map;
    }

    /**
     * @param array<int,string> $row
     * @param array<string,int> $map
     * @return array<string,string>
     */
    private function extractFieldsFromRow(array $row, array $map, string $emptyCells, string $clearToken): array
    {
        $fields = [];

        foreach ($map as $field => $idx) {
            if (in_array($field, ['term_id', 'taxonomy', 'term_slug', 'term_name', 'term_link'], true)) {
                continue;
            }

            $raw = isset($row[$idx]) ? (string) $row[$idx] : '';
            $trimmed = trim($raw);

            if ($clearToken !== '' && strcasecmp($trimmed, $clearToken) === 0) {
                $fields[$field] = self::CLEAR_MARKER;
                continue;
            }

            if ($trimmed === '' && $emptyCells === 'ignore') {
                continue;
            }

            $fields[$field] = $this->sanitizeIncomingValue($field, $trimmed);
        }

        return $fields;
    }

    private function sanitizeIncomingValue(string $field, string $value): string
    {
        $value = trim($value);

        switch ($field) {
            case 'seo_title':
                return sanitize_text_field($value);
            case 'meta_description':
                return sanitize_textarea_field($value);
            case 'canonical':
                return $this->sanitizeUrlAllowRelative($value);
            case 'robots_index':
                return $this->sanitizeRobotsIndex($value);
            case 'robots_follow':
                return $this->sanitizeRobotsFollow($value);
            case 'og_title':
                return sanitize_text_field($value);
            case 'og_description':
                return sanitize_textarea_field($value);
            case 'og_image':
                return $this->sanitizeUrlAllowRelative($value);
            case 'twitter_title':
                return sanitize_text_field($value);
            case 'twitter_description':
                return sanitize_textarea_field($value);
            case 'twitter_image':
                return $this->sanitizeUrlAllowRelative($value);
            default:
                return '';
        }
    }

    private function sanitizeRobotsIndex(string $value): string
    {
        $value = strtolower(trim($value));
        $allowed = ['default', 'index', 'noindex', ''];
        return in_array($value, $allowed, true) ? $value : '';
    }

    private function sanitizeRobotsFollow(string $value): string
    {
        $value = strtolower(trim($value));
        $allowed = ['default', 'follow', 'nofollow', ''];
        return in_array($value, $allowed, true) ? $value : '';
    }

    private function sanitizeUrlAllowRelative(string $url): string
    {
        $url = trim($url);
        if ($url === '') {
            return '';
        }
        if (preg_match('~^https?://~i', $url)) {
            return esc_url_raw($url);
        }
        if (str_starts_with($url, '/')) {
            return $url;
        }
        return '';
    }

    /**
     * @param array<int,string> $errors
     * @return array<int,array<int,string>>
     */
    private function readCsv(string $filePath, array &$errors): array
    {
        $rows = [];

        $fh = fopen($filePath, 'r');
        if (!$fh) {
            $errors[] = __('Could not open uploaded CSV.', 'wp-seo-next');
            return [];
        }

        // Detect delimiter by sampling the first line.
        $first = fgets($fh);
        if ($first === false) {
            fclose($fh);
            return [];
        }

        $delims = [',', ';', "\t"];
        $best = ',';
        $bestCount = 0;
        foreach ($delims as $d) {
            $c = substr_count($first, $d);
            if ($c > $bestCount) {
                $bestCount = $c;
                $best = $d;
            }
        }

        rewind($fh);

        while (($data = fgetcsv($fh, 0, $best)) !== false) {
            if (!is_array($data)) {
                continue;
            }
            $rows[] = array_map('trim', $data);
        }

        fclose($fh);

        return $rows;
    }
}
