<?php

declare(strict_types=1);

namespace BadamSoft\WooProductExporter\Exporter\Meta;

use BadamSoft\WooProductExporter\Helpers\Utils;
use wpdb;

use function apply_filters;
use function array_filter;
use function array_fill_keys;
use function array_map;
use function function_exists;
use function is_array;
use function is_string;
use function preg_match;
use function stripos;
use function substr;
use function trim;

/**
 * Detects Advanced Custom Fields (ACF) and arbitrary post meta fields for products.
 */
class MetaFieldDetector {
    private const META_SCAN_LIMIT = 2000;

    /**
     * Meta keys that are already covered by dedicated exporter fields and should be ignored.
     *
     * @var array<int, string>
     */
    private const RESERVED_META_KEYS = [
        '_price',
        '_regular_price',
        '_sale_price',
        '_sale_price_dates_from',
        '_sale_price_dates_to',
        '_sku',
        '_stock',
        '_stock_status',
        '_manage_stock',
        '_backorders',
        '_sold_individually',
        '_weight',
        '_length',
        '_width',
        '_height',
        '_tax_status',
        '_tax_class',
        '_thumbnail_id',
        '_product_image_gallery',
        '_downloadable',
        '_virtual',
        '_featured',
        '_downloadable_files',
        '_download_limit',
        '_download_expiry',
        '_product_attributes',
        '_product_version',
        '_edit_lock',
        '_edit_last',
        '_wc_review_count',
        '_wc_average_rating',
        '_visibility',
        '_default_attributes',
    ];

    private wpdb $db;

    /**
     * @var array<string, mixed>|null
     */
    private ?array $cache = null;

    public function __construct( wpdb $db ) {
        $this->db = $db;
    }

    /**
     * @return array{acf: array<int, array<string, mixed>>, meta: array<int, array<string, mixed>>}
     */
    public function detect(): array {
        if ( null !== $this->cache ) {
            return $this->cache;
        }

        $acf_fields  = $this->detectAcfFields();
        $acf_roots   = [];

        foreach ( $acf_fields as $descriptor ) {
            $root = $descriptor['root'] ?? '';

            if ( '' !== $root ) {
                $acf_roots[ $root ] = true;
            }
        }

        $meta_fields = $this->detectMetaFields( array_keys( $acf_roots ) );

        $this->cache = [
            'acf'  => array_values( $acf_fields ),
            'meta' => array_values( $meta_fields ),
        ];

        return $this->cache;
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    private function detectAcfFields(): array {
        if ( ! function_exists( 'acf_get_field_groups' ) || ! function_exists( 'acf_get_fields' ) ) {
            return [];
        }

        $field_groups = acf_get_field_groups(
            [
                'post_type' => [ 'product', 'product_variation' ],
            ]
        );

        if ( empty( $field_groups ) || ! is_array( $field_groups ) ) {
            return [];
        }

        $detected = [];

        foreach ( $field_groups as $group ) {
            if ( ! is_array( $group ) ) {
                continue;
            }

            $fields = acf_get_fields( $group );

            if ( empty( $fields ) || ! is_array( $fields ) ) {
                continue;
            }

            $group_title = isset( $group['title'] ) && is_string( $group['title'] ) ? $group['title'] : '';

            $this->collectAcfFields( $fields, '', $detected, $group_title, null );
        }

        return apply_filters( 'prodexfo_detected_acf_fields', $detected, $field_groups, $this->db );
    }

    /**
     * @param array<string, mixed> $field
     * @return array<int, mixed>
     */
    private function resolveCloneSubFields( array $field ): array {
        $clone = isset( $field['clone'] ) && is_array( $field['clone'] ) ? $field['clone'] : [];

        if ( empty( $clone ) ) {
            return [];
        }

        $resolved = [];

        foreach ( $clone as $item ) {
            if ( is_string( $item ) ) {
                $referenced_field = function_exists( 'acf_get_field' ) ? acf_get_field( $item ) : null;

                if ( is_array( $referenced_field ) ) {
                    $resolved[] = $referenced_field;
                }

                continue;
            }

            if ( is_array( $item ) ) {
                $resolved[] = $item;
            }
        }

        return $resolved;
    }

    /**
     * @param array<int, mixed>            $fields
     * @param array<string, mixed>         $bucket
     */
    private function collectAcfFields( array $fields, string $prefix, array &$bucket, string $group_title, ?string $root ): void {
        foreach ( $fields as $field ) {
            if ( ! is_array( $field ) ) {
                continue;
            }

            $name = isset( $field['name'] ) && is_string( $field['name'] ) ? trim( $field['name'] ) : '';

            if ( '' === $name ) {
                continue;
            }

            $type = isset( $field['type'] ) && is_string( $field['type'] ) ? $field['type'] : 'text';
            $path = '' === $prefix ? $name : $prefix . '.' . $name;
            $current_root = $root ?? $name;

            if ( in_array( $type, [ 'group', 'repeater' ], true ) ) {
                $sub_fields = isset( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ? $field['sub_fields'] : [];

                if ( ! empty( $sub_fields ) ) {
                    $this->collectAcfFields( $sub_fields, $path, $bucket, $group_title, $current_root );
                }

                continue;
            }

            if ( 'flexible_content' === $type ) {
                $layouts = isset( $field['layouts'] ) && is_array( $field['layouts'] ) ? $field['layouts'] : [];

                foreach ( $layouts as $layout ) {
                    if ( ! is_array( $layout ) ) {
                        continue;
                    }

                    $layout_name = isset( $layout['name'] ) && is_string( $layout['name'] ) && '' !== trim( $layout['name'] )
                        ? trim( $layout['name'] )
                        : ( isset( $layout['label'] ) ? sanitize_key( (string) $layout['label'] ) : 'layout' );

                    $layout_path   = $path . '.' . $layout_name;
                    $layout_fields = isset( $layout['sub_fields'] ) && is_array( $layout['sub_fields'] ) ? $layout['sub_fields'] : [];

                    if ( ! empty( $layout_fields ) ) {
                        $this->collectAcfFields( $layout_fields, $layout_path, $bucket, $group_title, $current_root );
                    }
                }

                continue;
            }

            if ( 'clone' === $type ) {
                $referenced = $this->resolveCloneSubFields( $field );

                if ( ! empty( $referenced ) ) {
                    $this->collectAcfFields( $referenced, $prefix, $bucket, $group_title, $root );
                }

                continue;
            }

            $bucket_key = 'acf|' . $path;

            if ( isset( $bucket[ $bucket_key ] ) ) {
                continue;
            }
            $bucket[ $bucket_key ] = [
                'field_key'      => $bucket_key,
                'path'           => $path,
                'label'          => $this->buildAcfLabel( $group_title, $field ),
                'acf_type'       => $type,
                'root'           => $current_root,
                'group_title'    => $group_title,
                'acf_field'      => $field,
                'acf_field_key'  => isset( $field['key'] ) && is_string( $field['key'] ) ? $field['key'] : '',
            ];
        }
    }

    /**
     * @param array<int, string> $acf_roots
     * @return array<string, array<string, mixed>>
     */
    private function detectMetaFields( array $acf_roots ): array {
        $post_types   = [ 'product', 'product_variation' ];
        $placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
        $limit        = $this->getMetaScanLimit();

        $query = $this->db->prepare(
            "SELECT DISTINCT pm.meta_key
             FROM {$this->db->postmeta} AS pm
             INNER JOIN {$this->db->posts} AS p ON p.ID = pm.post_id
             WHERE p.post_type IN ($placeholders)
             ORDER BY pm.meta_key
             LIMIT %d",
            [ ...$post_types, $limit ]
        );

        $meta_keys = $query ? $this->db->get_col( $query ) : [];

        if ( empty( $meta_keys ) ) {
            return [];
        }

        $meta_keys_map = array_fill_keys( array_map( 'strval', $meta_keys ), true );
        $acf_root_map  = array_fill_keys( $acf_roots, true );
        $reserved_map  = array_fill_keys( $this->getReservedMetaKeys(), true );
        $detected      = [];

        foreach ( $meta_keys as $meta_key ) {
            if ( ! is_string( $meta_key ) ) {
                continue;
            }

            $meta_key = trim( $meta_key );

            if ( $this->shouldSkipMetaKey( $meta_key, $acf_root_map, $reserved_map, $meta_keys_map ) ) {
                continue;
            }

            $label = Utils::humanize_meta_key( $meta_key );
            $is_private = ( '' !== $meta_key && '_' === $meta_key[0] );

            if ( $is_private && false === stripos( $label, $meta_key ) ) {
                $label .= ' (' . $meta_key . ')';
            }

            $key   = 'meta|' . $meta_key;

            $detected[ $key ] = [
                'field_key' => $key,
                'meta_key'  => $meta_key,
                'label'     => $label,
                'is_private'=> $is_private,
            ];
        }

        return apply_filters( 'prodexfo_detected_meta_fields', $detected, $acf_roots, $this->db );
    }

    /**
     * @param array<string, bool> $acf_root_map
     * @param array<string, bool> $reserved_map
     */
    private function shouldSkipMetaKey( string $meta_key, array $acf_root_map, array $reserved_map, array $meta_keys_map ): bool {
        if ( '' === $meta_key ) {
            return true;
        }

        if ( isset( $reserved_map[ $meta_key ] ) ) {
            return true;
        }

        if ( isset( $acf_root_map[ $meta_key ] ) ) {
            return true;
        }

        if ( $this->isAcfReferenceMetaKey( $meta_key, $meta_keys_map ) ) {
            return true;
        }

        return false;
    }

    private function getMetaScanLimit(): int {
        $limit = (int) apply_filters( 'prodexfo_meta_scan_limit', self::META_SCAN_LIMIT );

        return $limit > 0 ? $limit : self::META_SCAN_LIMIT;
    }

    /**
     * @return array<int, string>
     */
    private function getReservedMetaKeys(): array {
        $reserved = apply_filters( 'prodexfo_reserved_meta_keys', self::RESERVED_META_KEYS );

        return array_values( array_unique( array_filter( array_map( 'strval', (array) $reserved ) ) ) );
    }

    private function isAcfReferenceMetaKey( string $meta_key, array $meta_keys_map ): bool {
        if ( '' === $meta_key ) {
            return false;
        }

        if ( '_' !== $meta_key[0] ) {
            return false;
        }

        $base = substr( $meta_key, 1 );

        if ( '' !== $base && isset( $meta_keys_map[ $base ] ) ) {
            return true;
        }

        if ( preg_match( '/^_field_/', $meta_key ) ) {
            return true;
        }

        $override = apply_filters( 'prodexfo_is_reference_meta_key', null, $meta_key, $meta_keys_map );

        if ( null !== $override ) {
            return (bool) $override;
        }

        return false;
    }

    /**
     * @param array<string, mixed> $field
     */
    private function buildAcfLabel( string $group_title, array $field ): string {
        $label = isset( $field['label'] ) && is_string( $field['label'] ) ? trim( $field['label'] ) : '';

        if ( '' === $label ) {
            $label = isset( $field['name'] ) && is_string( $field['name'] ) ? Utils::humanize_meta_key( $field['name'] ) : 'ACF Field';
        }

        if ( '' !== $group_title ) {
            return $group_title . ' → ' . $label;
        }

        return $label;
    }
}
