<?php
/**
 * Plugin Name: Migratik
 * Plugin URI: https://hossamhamdy99.github.io/migratik-site/
 * Description: Pack up any WordPress site — content, SEO signals, internal links, and images — into one clean file for migration. Builder-agnostic (Gutenberg, Classic, WPBakery, Divi, Elementor, Beaver Builder, Bricks, Oxygen). Free: JSON export. Pro: Markdown/MDX, WXR, CSV, and a one-click media bundle. Read-only; safe to delete after use.
 * Version: 1.3.2
 * Requires at least: 5.6
 * Requires PHP: 7.4
 * Author: Hossam Hamdy
 * Author URI: https://github.com/hossamhamdy99
 * License: GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: migratik
 * Domain Path: /languages
 */
if (!defined('ABSPATH')) exit;

/*
 * Legacy-coexistence guard: if the OLD free build (folder "migratik", pre-SDK)
 * is still active while THIS file is a different build (e.g. "migratik-premium"),
 * declare nothing on this request — just schedule the legacy copy for silent
 * deactivation and bail. On the next request this build takes over cleanly.
 * Prevents duplicate-function fatals when customers upgrade from old versions.
 */
if (function_exists('get_option')
    && plugin_basename(__FILE__) !== 'migratik/migratik.php'
    && in_array('migratik/migratik.php', (array) get_option('active_plugins', []), true)) {
    add_action('plugins_loaded', function () {
        if (!function_exists('deactivate_plugins')) require_once ABSPATH . 'wp-admin/includes/plugin.php';
        deactivate_plugins('migratik/migratik.php', true);
    }, 0);
    return;
}

if (!defined('MIGRATIK_VERSION')) define('MIGRATIK_VERSION', '1.3.2');
if (!defined('MIGRATIK_OPTION'))  define('MIGRATIK_OPTION', 'migratik_settings');
if (!defined('MIGRATIK_DIR'))     define('MIGRATIK_DIR', plugin_dir_path(__FILE__));

/* -------------------------------------------------------------------------
 * Licensing (Freemius) — one plugin, premium features unlocked by a license.
 *
 * The plugin runs fully as the free tier with NO Freemius SDK present. To sell the
 * Pro tier, drop the Freemius SDK into /freemius and paste the init snippet from your
 * Freemius dashboard below (see freemius/README-FREEMIUS.md). Until then, Pro features
 * stay locked — except when MIGRATIK_PRO is defined true (for your own testing).
 * ---------------------------------------------------------------------- */
if (!function_exists('migratik_fs')) {
    function migratik_fs() {
        global $migratik_fs;
        if (!isset($migratik_fs)) {
            $sdk = MIGRATIK_DIR . 'freemius/start.php';
            // WPINC guard: only boot the SDK inside a real WordPress runtime
            // (keeps CLI harnesses and static analysis from loading it).
            if (file_exists($sdk) && defined('WPINC')) {
                require_once $sdk;
                $migratik_fs = fs_dynamic_init([
                    'id'                  => '35072',
                    'slug'                => 'migratik',
                    'premium_suffix'      => 'Pro',
                    'type'                => 'plugin',
                    'public_key'          => 'pk_d88da8e9cf140c76240498f2acdc4',
                    'is_premium'          => false,
                    'has_premium_version' => true,
                    'has_addons'          => false,
                    'has_paid_plans'      => true,
                    'is_org_compliant'    => true,
                    'menu'                => [
                        'slug'    => 'migratik',
                        'account' => true,
                        'contact' => false,
                        'support' => false,
                    ],
                ]);
                do_action('migratik_fs_loaded');
                // Uninstall cleanup via the Freemius lifecycle (replaces uninstall.php,
                // which the Freemius deployment pipeline does not allow).
                $migratik_fs->add_action('after_uninstall', 'migratik_fs_uninstall_cleanup');
            }
        }
        return isset($migratik_fs) ? $migratik_fs : null;
    }
    migratik_fs();
}

/**
 * Delete the single option Migratik stores. Runs on uninstall through the
 * Freemius 'after_uninstall' hook. Multisite-aware; the plugin is read-only,
 * so there is nothing else (tables, post meta, CPTs) to clean up.
 */
function migratik_fs_uninstall_cleanup() {
    $option = 'migratik_settings';
    if (is_multisite()) {
        $site_ids = get_sites(['fields' => 'ids', 'number' => 0]);
        foreach ($site_ids as $site_id) {
            switch_to_blog($site_id);
            delete_option($option);
            restore_current_blog();
        }
    } else {
        delete_option($option);
    }
}

/** True when the current site may use Pro features (a valid license, or the dev override). */
function migratik_can_use_pro() {
    if (defined('MIGRATIK_PRO') && MIGRATIK_PRO) return true;              // manual override for testing
    $fs = function_exists('migratik_fs') ? migratik_fs() : null;
    if ($fs && method_exists($fs, 'can_use_premium_code')) return (bool) $fs->can_use_premium_code();
    return (bool) apply_filters('migratik_can_use_pro', false);
}

/** The export formats available in this build (JSON is always free; the rest are Pro). */
function migratik_formats() {
    return [
        'json' => ['label' => 'JSON (data only)',            'pro' => false],
        'md'   => ['label' => 'Markdown (.md)',              'pro' => true],
        'mdx'  => ['label' => 'MDX (.mdx)',                  'pro' => true],
        'wxr'  => ['label' => 'WordPress WXR (.xml)',        'pro' => true],
        'csv'  => ['label' => 'CSV (one file per type)',     'pro' => true],
    ];
}

/*
 * REST API key resolution:
 *   1. If MIGRATIK_KEY is defined in wp-config.php, that value wins (locked, not editable in the UI).
 *   2. Otherwise Migratik stores a generated key in its settings and lets you rotate it under Tools → Migratik.
 * The admin Download button always uses a nonce and never needs the key — the key only guards the REST fallback.
 */

/* -------------------------------------------------------------------------
 * i18n
 * ---------------------------------------------------------------------- */
add_action('init', function () {
    load_plugin_textdomain('migratik', false, dirname(plugin_basename(__FILE__)) . '/languages');
});

/* -------------------------------------------------------------------------
 * Settings storage & helpers
 * ---------------------------------------------------------------------- */
function migratik_default_settings() {
    return [
        'post_types'          => [],
        'include_raw'         => true,
        'include_meta'        => true,
        'include_comments'    => true,
        'include_unpublished' => false,
        'rest_key'            => '',
    ];
}

function migratik_get_settings() {
    $s = get_option(MIGRATIK_OPTION, []);
    if (!is_array($s)) $s = [];
    return array_merge(migratik_default_settings(), $s);
}

function migratik_generate_key() {
    return 'mgk_' . bin2hex(random_bytes(16));
}

/** True when the key is pinned via the wp-config.php constant (and therefore not editable in the UI). */
function migratik_key_is_constant() {
    return defined('MIGRATIK_KEY') && MIGRATIK_KEY && MIGRATIK_KEY !== 'set-a-random-string-here';
}

/** Read-only: the effective REST key (the constant if set, otherwise the stored key). Empty when none is seeded yet. */
function migratik_get_key() {
    if (migratik_key_is_constant()) return (string) MIGRATIK_KEY;
    $s = migratik_get_settings();
    return isset($s['rest_key']) ? (string) $s['rest_key'] : '';
}

/** Seed a stored REST key once. Runs on activation and admin_init; never writes when the constant is set. */
function migratik_ensure_key() {
    if (migratik_key_is_constant()) return;
    $s = migratik_get_settings();
    if (empty($s['rest_key'])) {
        $s['rest_key'] = migratik_generate_key();
        update_option(MIGRATIK_OPTION, $s);
    }
}
register_activation_hook(__FILE__, 'migratik_ensure_key');
add_action('admin_init', 'migratik_ensure_key');

/* -------------------------------------------------------------------------
 * Post types
 * ---------------------------------------------------------------------- */
function migratik_all_post_types() {
    $skip = ['attachment','nav_menu_item','wp_block','wp_template','wp_template_part',
             'wp_global_styles','wp_navigation','wp_font_family','wp_font_face','custom_css',
             'customize_changeset','oembed_cache','user_request','revision'];
    return array_values(array_diff(get_post_types(['public'=>true],'names'), $skip));
}

/** The post types that will actually be exported: the configured subset, or all public types when unset. */
function migratik_export_post_types() {
    $all = migratik_all_post_types();
    $s   = migratik_get_settings();
    $sel = array_values(array_intersect($all, array_map('strval', (array) $s['post_types'])));
    return $sel ? $sel : $all;
}

/** Post statuses to export: just 'publish', or the full set when unpublished content is opted in. */
function migratik_export_statuses() {
    $s = migratik_get_settings();
    return !empty($s['include_unpublished'])
        ? ['publish', 'future', 'draft', 'pending', 'private']
        : ['publish'];
}

function migratik_manifest_data() {
    $statuses = migratik_export_statuses();
    $out = [];
    foreach (migratik_export_post_types() as $pt) {
        $c = wp_count_posts($pt);
        $n = 0;
        foreach ($statuses as $st) { if (isset($c->$st)) $n += (int) $c->$st; }
        $out[$pt] = $n;
    }
    return ['site'=>get_bloginfo('name'), 'post_types'=>$out];
}

/** Approved comments for one post, threaded via parent, oldest first. Emails are intentionally omitted. */
function migratik_comments($post_id) {
    if (!function_exists('get_comments')) return [];
    $out = [];
    $comments = get_comments([
        'post_id' => $post_id,
        'status'  => 'approve',
        'type'    => 'comment',
        'order'   => 'ASC',
        'orderby' => 'comment_date_gmt',
    ]);
    foreach ((array) $comments as $c) {
        $out[] = [
            'id'         => (int) $c->comment_ID,
            'author'     => $c->comment_author,
            'author_url' => $c->comment_author_url,
            'date'       => function_exists('mysql2date') ? mysql2date('c', $c->comment_date_gmt) : $c->comment_date_gmt,
            'content'    => $c->comment_content,
            'parent'     => (int) $c->comment_parent,
        ];
    }
    return $out;
}

/** Navigation menus (structure + items + theme locations) so the site's nav survives a migration. */
function migratik_menus() {
    if (!function_exists('wp_get_nav_menus')) return [];
    $locations = function_exists('get_nav_menu_locations') ? (array) get_nav_menu_locations() : [];
    $term_to_locations = [];
    foreach ($locations as $loc => $term_id) {
        $term_to_locations[(int) $term_id][] = $loc;
    }
    $out = [];
    foreach ((array) wp_get_nav_menus() as $menu) {
        $items = wp_get_nav_menu_items($menu->term_id);
        $mi = [];
        foreach ((array) ($items ?: []) as $it) {
            $mi[] = [
                'id'        => (int) $it->ID,
                'title'     => $it->title,
                'url'       => $it->url,
                'type'      => $it->type,        // post_type | taxonomy | custom
                'object'    => $it->object,      // page | category | custom | ...
                'object_id' => (int) $it->object_id,
                'parent'    => (int) $it->menu_item_parent,
                'order'     => (int) $it->menu_order,
                'target'    => $it->target,
                'classes'   => is_array($it->classes) ? array_values(array_filter($it->classes)) : [],
            ];
        }
        $out[] = [
            'name'      => $menu->name,
            'slug'      => $menu->slug,
            'locations' => isset($term_to_locations[(int) $menu->term_id]) ? $term_to_locations[(int) $menu->term_id] : [],
            'items'     => $mi,
        ];
    }
    return $out;
}

/* -------------------------------------------------------------------------
 * Content rendering (builder-agnostic) & extraction
 * ---------------------------------------------------------------------- */
function migratik_render_content($post, &$builder = null) {
    $id = $post->ID;
    // Page builders (Elementor especially) PRINT <style>/<link> tags while rendering.
    // Wrap every render path in output buffering and discard that stray output so it can
    // never be injected into the export stream — the meaningful HTML is the return value.
    if (did_action('elementor/loaded') && class_exists('\Elementor\Plugin') && isset(\Elementor\Plugin::$instance->frontend)) {
        if (get_post_meta($id, '_elementor_edit_mode', true) === 'builder') {
            ob_start();
            $html = \Elementor\Plugin::$instance->frontend->get_builder_content_for_display($id, false);
            ob_end_clean();
            if (is_string($html) && trim($html) !== '') { $builder = 'elementor'; return $html; }
        }
    }
    if (class_exists('FLBuilder') && get_post_meta($id, '_fl_builder_enabled', true) && method_exists('FLBuilder','render_content_by_id')) {
        ob_start();
        $ret = FLBuilder::render_content_by_id($id);
        $buf = ob_get_clean();
        $html = (is_string($ret) && trim($ret) !== '') ? $ret : $buf;
        if (is_string($html) && trim($html) !== '') { $builder = 'beaver'; return $html; }
    }
    // Bricks Builder — content lives in its own post meta and is rendered by the Bricks engine,
    // never through the_content. Render the stored element tree; discard any printed CSS/JS.
    if (class_exists('\Bricks\Frontend') && method_exists('\Bricks\Frontend', 'render_data')) {
        $elements = get_post_meta($id, '_bricks_page_content_2', true);
        if (is_array($elements) && !empty($elements)) {
            $html = '';
            ob_start();
            try {
                $html = call_user_func(['\Bricks\Frontend', 'render_data'], $elements);
            } catch (\Throwable $e) {
                $html = '';
            }
            $buf = ob_get_clean();
            $html = (is_string($html) && trim($html) !== '') ? $html : $buf;
            if (is_string($html) && trim($html) !== '') { $builder = 'bricks'; return $html; }
        }
    }
    // Oxygen Builder — content lives in Oxygen's meta (JSON on 4.0+, shortcodes on classic) and is
    // rendered by Oxygen's own pipeline, not the_content. Try JSON first, then classic shortcodes.
    $oxy_json = get_post_meta($id, '_ct_builder_json', true);
    $oxy_sc   = get_post_meta($id, 'ct_builder_shortcodes', true);
    if ((is_string($oxy_json) && trim($oxy_json) !== '') || (is_string($oxy_sc) && trim($oxy_sc) !== '')) {
        $html = '';
        ob_start();
        try {
            if (is_string($oxy_json) && trim($oxy_json) !== '' && function_exists('do_oxygen_elements')) {
                $decoded = json_decode($oxy_json, true);
                if (is_array($decoded)) { $html = do_oxygen_elements($decoded); }
            }
            if ((!is_string($html) || trim($html) === '') && is_string($oxy_sc) && trim($oxy_sc) !== '') {
                $html = do_shortcode($oxy_sc);
            }
        } catch (\Throwable $e) {
            $html = '';
        }
        $buf = ob_get_clean();
        $html = (is_string($html) && trim($html) !== '') ? $html : $buf;
        if (is_string($html) && trim($html) !== '') { $builder = 'oxygen'; return $html; }
    }
    $builder = 'default';
    ob_start();
    // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WordPress content filter
    $html = apply_filters('the_content', $post->post_content);
    ob_end_clean();
    return is_string($html) ? $html : '';
}

function migratik_seo_meta($all_meta) {
    $keep = [];
    foreach ((array)$all_meta as $k=>$v) {
        if (preg_match('/(seo|description|meta.?desc|title|keyword|canonical|og[:_]|twitter|schema|_yoast|rank[_ ]?math|_aioseo|focus)/i',$k)) {
            $val = is_array($v) ? ($v[0] ?? '') : $v;
            $keep[$k] = maybe_unserialize($val);
        }
    }
    return $keep;
}

/** Public custom fields (ACF / JetEngine / Pods, etc.) — everything not internal and not already captured as SEO. */
function migratik_custom_fields($all_meta, $exclude = []) {
    // Oxygen keeps its builder blobs in public (non-underscore) ct_* meta — skip them here;
    // they are already captured, rendered, in content_html. (Bricks/Elementor use _-prefixed keys.)
    $skip = array_merge(
        ['footnotes', 'ct_builder_shortcodes', 'ct_builder_json', 'ct_other_template', 'ct_parent_template'],
        (array) $exclude
    );
    $out  = [];
    foreach ((array) $all_meta as $k => $v) {
        if ($k === '' || $k[0] === '_') continue;      // internal / protected meta (e.g. _elementor_data)
        if (in_array($k, $skip, true)) continue;        // already captured as SEO, or noise
        $vals = array_map('maybe_unserialize', (array) $v);
        $out[$k] = (count($vals) === 1) ? $vals[0] : $vals;
    }
    return $out;
}

/**
 * Resolve custom-field values that are attachment IDs into full media objects.
 *
 * Image fields from ACF / JetEngine / Pods are stored in post meta as the bare
 * attachment ID (e.g. doctor-image => "768"), which is useless off-site. This
 * builds a PARALLEL map { field => media-object | [media-objects] } so a migration
 * can rewrite those ID references to real URLs. It is non-destructive: the raw IDs
 * stay in custom_fields untouched. Only values that actually resolve to an
 * attachment are included, so plain numbers (phone numbers, counts) are ignored.
 */
function migratik_resolve_media($custom_fields) {
    $media = [];
    foreach ((array) $custom_fields as $k => $v) {
        $ids      = migratik_media_ids($v);
        if (!$ids) continue;
        $resolved = [];
        foreach ($ids as $id) {
            $info = migratik_attachment_info($id);
            if ($info) $resolved[] = $info;
        }
        if (!$resolved) continue;
        // Scalar field -> single object; array/multi field -> list.
        $media[$k] = (!is_array($v) && count($resolved) === 1) ? $resolved[0] : $resolved;
    }
    return $media;
}

/** Candidate positive-integer attachment IDs from a scalar or array field value (max 9 digits, so long phone numbers are skipped). */
function migratik_media_ids($v) {
    $ids = [];
    foreach ((array) $v as $one) {
        if (is_int($one) && $one > 0) { $ids[] = $one; continue; }
        if (is_string($one) && preg_match('/^\d{1,9}$/', trim($one))) {
            $n = (int) trim($one);
            if ($n > 0) $ids[] = $n;
        }
    }
    return $ids;
}

/** Full media object for an attachment ID, or null when the ID is not an attachment on this site. */
function migratik_attachment_info($id) {
    if (get_post_type($id) !== 'attachment') return null;   // false for non-existent / non-attachment IDs
    $url = wp_get_attachment_url($id);
    if (!$url) return null;
    $info = [
        'id'   => (int) $id,
        'url'  => $url,
        'alt'  => (string) get_post_meta($id, '_wp_attachment_image_alt', true),
        'mime' => (string) get_post_mime_type($id),
    ];
    $meta = wp_get_attachment_metadata($id);
    if (is_array($meta)) {
        if (!empty($meta['width']))  $info['width']  = (int) $meta['width'];
        if (!empty($meta['height'])) $info['height'] = (int) $meta['height'];
    }
    return $info;
}

function migratik_extract($html, $home_host) {
    $internal=[]; $outbound=[]; $images=[];
    if (trim((string)$html)==='') return compact('internal','outbound','images');
    if (class_exists('DOMDocument')) {
        $dom = new DOMDocument();
        libxml_use_internal_errors(true);
        $dom->loadHTML('<?xml encoding="UTF-8"?><div>'.$html.'</div>');
        libxml_clear_errors();
        $hh = preg_replace('/^www\./i','',$home_host);
        foreach ($dom->getElementsByTagName('a') as $a) {
            $href = trim($a->getAttribute('href'));
            if ($href===''||$href[0]==='#'||stripos($href,'javascript:')===0||stripos($href,'mailto:')===0||stripos($href,'tel:')===0) continue;
            $text = trim(preg_replace('/\s+/u',' ', $a->textContent));
            $host = wp_parse_url($href, PHP_URL_HOST);
            $h = $host ? preg_replace('/^www\./i','',$host) : '';
            $rec = ['url'=>$href,'text'=>$text];
            if ($h==='' || strcasecmp($h,$hh)===0) $internal[]=$rec; else $outbound[]=$rec;
        }
        foreach ($dom->getElementsByTagName('img') as $img) {
            $src = trim($img->getAttribute('src'));
            if ($src==='') continue;
            $images[] = ['src'=>$src, 'alt'=>trim($img->getAttribute('alt'))];
        }
    }
    return compact('internal','outbound','images');
}

/* -------------------------------------------------------------------------
 * Build the export payload
 * ---------------------------------------------------------------------- */
function migratik_build_data() {
    if (function_exists('wp_raise_memory_limit')) wp_raise_memory_limit('admin');
    // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- large exports can exceed the default max_execution_time
    if (function_exists('set_time_limit')) set_time_limit(0);
    $home_host   = (string) wp_parse_url(home_url(), PHP_URL_HOST);
    $s0              = migratik_get_settings();
    $include_raw     = (bool) $s0['include_raw'];
    $include_meta    = (bool) $s0['include_meta'];
    $include_comments = (bool) $s0['include_comments'];
    $site_name   = get_bloginfo('name', 'display');
    $title_sep   = '-'; // separator for the deterministic per-item document_title
    $out = [
        'generator'=>'Migratik '.MIGRATIK_VERSION,
        'author'=>'Hossam Hamdy',
        'site'=>['name'=>get_bloginfo('name'),'description'=>get_bloginfo('description'),
                 'url'=>home_url('/'),'host'=>$home_host,'language'=>get_bloginfo('language')],
        'generated_at'=>current_time('c'),
        'items'=>[],
    ];
    $q = new WP_Query([
        'post_type'=>migratik_export_post_types(),'post_status'=>migratik_export_statuses(),'posts_per_page'=>-1,
        'no_found_rows'=>true,'ignore_sticky_posts'=>true,'orderby'=>'date','order'=>'ASC',
    ]);
    foreach ($q->posts as $p) {
        $GLOBALS['post'] = $p;
        setup_postdata($p);
        $all_post_meta = get_post_meta($p->ID);
        $item_seo      = migratik_seo_meta($all_post_meta);
        $builder = 'default';
        $rendered = migratik_render_content($p, $builder);
        $links = migratik_extract($rendered, $home_host);
        $featured=null;
        if ($tid = get_post_thumbnail_id($p->ID)) {
            $src = wp_get_attachment_image_src($tid,'full');
            $featured = ['url'=>$src?$src[0]:null,'alt'=>get_post_meta($tid,'_wp_attachment_image_alt',true),
                         'width'=>$src?$src[1]:null,'height'=>$src?$src[2]:null];
        }
        $taxes=[];
        foreach (get_object_taxonomies($p->post_type) as $tax) {
            $terms = wp_get_post_terms($p->ID,$tax,['fields'=>'all']);
            if (!is_wp_error($terms)&&$terms) $taxes[$tax]=array_map(function($t){return ['id'=>$t->term_id,'name'=>$t->name,'slug'=>$t->slug];},$terms);
        }
        $post_title = get_the_title($p);
        $item = [
            'id'=>$p->ID,'type'=>$p->post_type,'status'=>$p->post_status,'slug'=>$p->post_name,'permalink'=>get_permalink($p),
            'title'=>$post_title,
            // Deterministic per-item title ("Post Title - Site Name"); any custom SEO title is preserved in seo_meta.
            'document_title'=>trim($post_title.' '.$title_sep.' '.$site_name),
            'builder'=>$builder,
            'excerpt'=>has_excerpt($p)?get_the_excerpt($p):'',
            'content_html'=>$rendered,
        ];
        if ($include_raw) $item['content_raw'] = $p->post_content;
        $item += [
            'date'=>get_the_date('c',$p),'modified'=>get_the_modified_date('c',$p),
            'featured_image'=>$featured,'taxonomies'=>$taxes,
            'internal_links'=>$links['internal'],'outbound_links'=>$links['outbound'],'images'=>$links['images'],
            'menu_order'=>$p->menu_order,'parent'=>$p->post_parent,
            'seo_meta'=>$item_seo,
        ];
        if ($include_meta) {
            $item['custom_fields'] = migratik_custom_fields($all_post_meta, array_keys($item_seo));
            // Resolve any image/attachment IDs stored in those fields to real media objects.
            $media = migratik_resolve_media($item['custom_fields']);
            if ($media) $item['custom_fields_media'] = $media;
        }
        if ($include_comments) {
            $comments = migratik_comments($p->ID);
            if ($comments) $item['comments'] = $comments;
        }
        $out['items'][] = $item;
    }
    wp_reset_postdata();
    $out['menus'] = migratik_menus();
    return $out;
}

function migratik_export_basename() {
    $host = wp_parse_url(home_url(), PHP_URL_HOST);
    $host = $host ? preg_replace('/^www\./i','',$host) : 'site';
    return sanitize_file_name($host) . '-migratik';
}

function migratik_filename() {
    return migratik_export_basename() . '.json';
}

/** Load the Pro formatter + media classes on demand (premium-only files, stripped from the free build). */
function migratik_load_pro_classes() {
    foreach (array('markdown', 'wxr', 'csv', 'bundle') as $mod) {
        $file = MIGRATIK_DIR . 'includes/class-migratik-' . $mod . '__premium_only.php';
        if (file_exists($file)) require_once $file;
    }
    return class_exists('Migratik_Markdown') && class_exists('Migratik_WXR')
        && class_exists('Migratik_CSV') && class_exists('Migratik_Bundle');
}

/**
 * Render an export into the files a given format produces: an associative
 * array of "path/inside/bundle" => file contents. JSON and WXR yield a single
 * file; Markdown/MDX/CSV yield many.
 */
function migratik_render_content_files($data, $format) {
    $site  = isset($data['site'])  ? $data['site']  : [];
    $items = isset($data['items']) ? $data['items'] : [];
    $out   = [];
    switch ($format) {
        case 'md':
        case 'mdx':
            $is_mdx = ($format === 'mdx');
            $ext    = $is_mdx ? 'mdx' : 'md';
            foreach ($items as $it) {
                $name = preg_replace('/\.md$/', '.' . $ext, Migratik_Markdown::filename($it));
                $out[$name] = Migratik_Markdown::render_item($it, $site, $is_mdx);
            }
            break;
        case 'wxr':
            $out[migratik_export_basename() . '.wxr'] = Migratik_WXR::render($data);
            break;
        case 'csv':
            foreach (Migratik_CSV::render($data) as $type => $text) {
                $out['csv/' . preg_replace('/[^a-z0-9\.\-_]/i', '-', $type) . '.csv'] = $text;
            }
            break;
        case 'json':
        default:
            $out['content.json'] = wp_json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            break;
    }
    return $out;
}

/* -------------------------------------------------------------------------
 * Settings registration (Settings API)
 * ---------------------------------------------------------------------- */
add_action('admin_init', function () {
    register_setting('migratik_settings_group', MIGRATIK_OPTION, [
        'type'              => 'array',
        'sanitize_callback' => 'migratik_sanitize_settings',
        'default'           => migratik_default_settings(),
    ]);
});

function migratik_sanitize_settings($input) {
    $current = migratik_get_settings();
    $out     = migratik_default_settings();

    $all = migratik_all_post_types();
    $pts = (isset($input['post_types']) && is_array($input['post_types'])) ? $input['post_types'] : [];
    $out['post_types'] = array_values(array_intersect($all, array_map('sanitize_key', $pts)));

    $out['include_raw']         = !empty($input['include_raw']);
    $out['include_meta']        = !empty($input['include_meta']);
    $out['include_comments']    = !empty($input['include_comments']);
    $out['include_unpublished'] = !empty($input['include_unpublished']);

    $regenerate = !empty($input['regenerate_key']);
    if (migratik_key_is_constant()) {
        // Constant wins; keep whatever is stored (unused) untouched.
        $out['rest_key'] = isset($current['rest_key']) ? $current['rest_key'] : '';
    } elseif ($regenerate || empty($current['rest_key'])) {
        $out['rest_key'] = migratik_generate_key();
        if ($regenerate) {
            add_settings_error(MIGRATIK_OPTION, 'migratik_key_rotated', __('A new REST API key was generated.', 'migratik'), 'updated');
        }
    } else {
        $out['rest_key'] = $current['rest_key'];
    }
    return $out;
}

/* -------------------------------------------------------------------------
 * Admin screen (Tools → Migratik): export panel + settings
 * ---------------------------------------------------------------------- */
add_action('admin_menu', function () {
    // Top-level menu with the Migratik brand mark (monochrome SVG, tinted by the admin color scheme).
    $migratik_icon = 'data:image/svg+xml;base64,' . base64_encode(
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="#a7aaad"><path d="M9 38V10h7.4L24 19.6 31.6 10H39v28h-6.2V19.4L24 30.4 15.2 19.4V38z"/><path d="M17.5 31.5h13L24 40.5z"/></svg>'
    );
    add_menu_page('Migratik', 'Migratik', 'manage_options', 'migratik', 'migratik_admin_page', $migratik_icon, '75.5');
});

add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) {
    $url = admin_url('admin.php?page=migratik');
    array_unshift($links, '<a href="' . esc_url($url) . '">' . esc_html__('Settings', 'migratik') . '</a>');
    return $links;
});

function migratik_admin_page() {
    if (!current_user_can('manage_options')) return;
    $m         = migratik_manifest_data();
    $total     = array_sum($m['post_types']);
    $settings  = migratik_get_settings();
    $all_types = migratik_all_post_types();

    echo '<div class="wrap">';
    echo '<h1>' . esc_html__('Migratik', 'migratik') . '</h1>';
    settings_errors();

    echo '<p>' . esc_html__('Pack up this site — all content (articles, services, pages, custom types) with SEO signals, internal links, and images — into one clean JSON file for migration.', 'migratik') . '</p>';
    echo '<p><em>' . esc_html__('Builder-agnostic: Gutenberg, Classic editor, WPBakery, Divi, Elementor, Beaver Builder, Bricks, Oxygen.', 'migratik') . '</em></p>';

    /* ---- What will be packed ---- */
    /* translators: %d: number of content items that will be exported. */
    echo '<h2>' . sprintf(esc_html__('What will be packed (%d items)', 'migratik'), (int) $total) . '</h2>';
    echo '<table class="widefat striped" style="max-width:520px"><thead><tr><th>' . esc_html__('Content type', 'migratik') . '</th><th>' . esc_html__('Published', 'migratik') . '</th></tr></thead><tbody>';
    if ($m['post_types']) {
        foreach ($m['post_types'] as $t => $c) {
            echo '<tr><td><code>' . esc_html($t) . '</code></td><td>' . (int) $c . '</td></tr>';
        }
    } else {
        echo '<tr><td colspan="2">' . esc_html__('No content types are selected — check at least one under Settings below.', 'migratik') . '</td></tr>';
    }
    echo '</tbody></table>';

    /* ---- Export ---- */
    $pro = migratik_can_use_pro();
    echo '<h2 style="margin-top:20px">' . esc_html__('Export', 'migratik') . '</h2>';

    // After a large media bundle is split, show the numbered part buttons.
    $pp_parts = isset($_GET['pp_parts']) ? (int) $_GET['pp_parts'] : 0;
    if ($pp_parts > 1) {
        $pf = isset($_GET['pp_format']) ? sanitize_key($_GET['pp_format']) : 'json';
        echo '<div class="notice notice-info inline"><p><strong>' . sprintf(esc_html__('Your media library is large, so the bundle is split into %d parts.', 'migratik'), $pp_parts) . '</strong> ' . esc_html__('Download each part and unzip them all into the same folder.', 'migratik') . '</p><p>';
        for ($i = 1; $i <= $pp_parts; $i++) {
            echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block;margin:0 6px 6px 0">';
            echo '<input type="hidden" name="action" value="migratik_download">';
            echo '<input type="hidden" name="format" value="' . esc_attr($pf) . '">';
            echo '<input type="hidden" name="bundle_media" value="1">';
            echo '<input type="hidden" name="part" value="' . (int) $i . '">';
            echo '<input type="hidden" name="confirm_part" value="1">';
            wp_nonce_field('migratik_download');
            echo '<button type="submit" class="button">' . sprintf(esc_html__('Part %1$d of %2$d', 'migratik'), $i, $pp_parts) . '</button>';
            echo '</form>';
        }
        echo '</p></div>';
    }

    echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
    echo '<input type="hidden" name="action" value="migratik_download">';
    wp_nonce_field('migratik_download');

    echo '<p><label for="migratik-format"><strong>' . esc_html__('Format', 'migratik') . '</strong></label><br>';
    echo '<select name="format" id="migratik-format" class="regular-text" style="max-width:320px">';
    foreach (migratik_formats() as $key => $f) {
        $disabled = ($f['pro'] && !$pro) ? ' disabled' : '';
        $suffix   = $f['pro'] ? ' — Pro' : '';
        echo '<option value="' . esc_attr($key) . '"' . $disabled . '>' . esc_html($f['label'] . $suffix) . '</option>';
    }
    echo '</select></p>';

    echo '<p><label><input type="checkbox" name="bundle_media" value="1"' . ($pro ? '' : ' disabled') . '> ';
    echo esc_html__('Include all media files — bundle images and rewrite URLs to the packaged copies', 'migratik');
    if (!$pro) echo ' <em>' . esc_html__('(Pro)', 'migratik') . '</em>';
    echo '</label></p>';

    echo '<p><button type="submit" class="button button-primary button-hero">&#11015; ' . esc_html__('Download export', 'migratik') . '</button></p>';
    echo '</form>';
    echo '<p class="description">' . esc_html__('JSON is free. Markdown/MDX, WXR, CSV, and the media bundle are Pro. Read-only — nothing on your site is changed.', 'migratik') . '</p>';

    if (!$pro) {
        $fs = function_exists('migratik_fs') ? migratik_fs() : null;
        $upg = ($fs && method_exists($fs, 'get_upgrade_url')) ? $fs->get_upgrade_url() : '';
        if ($upg) {
            echo '<p><a class="button button-secondary" href="' . esc_url($upg) . '">' . esc_html__('Unlock Pro formats & media bundling →', 'migratik') . '</a></p>';
        }
    }

    /* ---- Settings ---- */
    echo '<hr style="margin-top:26px">';
    echo '<h2>' . esc_html__('Settings', 'migratik') . '</h2>';
    echo '<form method="post" action="' . esc_url(admin_url('options.php')) . '">';
    settings_fields('migratik_settings_group');
    echo '<table class="form-table" role="presentation">';

    /* Post types */
    echo '<tr><th scope="row">' . esc_html__('Content types to export', 'migratik') . '</th><td><fieldset>';
    echo '<p class="description" style="margin:0 0 8px">' . esc_html__('Leave everything unchecked to export all public types (the default).', 'migratik') . '</p>';
    foreach ($all_types as $pt) {
        $obj     = get_post_type_object($pt);
        $label   = ($obj && isset($obj->labels->name)) ? $obj->labels->name : $pt;
        echo '<label style="display:inline-block;min-width:220px;margin:2px 0">';
        echo '<input type="checkbox" name="' . esc_attr(MIGRATIK_OPTION) . '[post_types][]" value="' . esc_attr($pt) . '"' . checked(in_array($pt, (array) $settings['post_types'], true), true, false) . '> ';
        echo esc_html($label) . ' <code>' . esc_html($pt) . '</code></label>';
    }
    echo '</fieldset></td></tr>';

    /* Raw content toggle */
    echo '<tr><th scope="row">' . esc_html__('Raw content', 'migratik') . '</th><td>';
    echo '<label><input type="checkbox" name="' . esc_attr(MIGRATIK_OPTION) . '[include_raw]" value="1"' . (!empty($settings['include_raw']) ? ' checked' : '') . '> ';
    echo esc_html__('Include the original post_content (content_raw) alongside the rendered content_html.', 'migratik') . '</label>';
    echo '<p class="description">' . esc_html__('Turn this off for a smaller file when you only need the rendered HTML.', 'migratik') . '</p>';
    echo '</td></tr>';

    /* Custom fields toggle */
    echo '<tr><th scope="row">' . esc_html__('Custom fields', 'migratik') . '</th><td>';
    echo '<label><input type="checkbox" name="' . esc_attr(MIGRATIK_OPTION) . '[include_meta]" value="1"' . (!empty($settings['include_meta']) ? ' checked' : '') . '> ';
    echo esc_html__('Capture public custom fields (ACF, JetEngine, Pods, and other post meta) — where custom post types keep their real data.', 'migratik') . '</label>';
    echo '<p class="description">' . esc_html__('Recommended on. Turn off only for a smaller file.', 'migratik') . '</p>';
    echo '</td></tr>';

    /* Comments toggle */
    echo '<tr><th scope="row">' . esc_html__('Comments', 'migratik') . '</th><td>';
    echo '<label><input type="checkbox" name="' . esc_attr(MIGRATIK_OPTION) . '[include_comments]" value="1"' . (!empty($settings['include_comments']) ? ' checked' : '') . '> ';
    echo esc_html__('Capture approved comments on each item (author, date, text, and threading).', 'migratik') . '</label>';
    echo '<p class="description">' . esc_html__('Commenter email addresses are never included.', 'migratik') . '</p>';
    echo '</td></tr>';

    /* Unpublished content toggle */
    echo '<tr><th scope="row">' . esc_html__('Unpublished content', 'migratik') . '</th><td>';
    echo '<label><input type="checkbox" name="' . esc_attr(MIGRATIK_OPTION) . '[include_unpublished]" value="1"' . (!empty($settings['include_unpublished']) ? ' checked' : '') . '> ';
    echo esc_html__('Also export drafts, pending, private, and scheduled items (each carries its status).', 'migratik') . '</label>';
    echo '<p class="description">' . esc_html__('Off by default — only published content is exported.', 'migratik') . '</p>';
    echo '</td></tr>';

    /* REST key */
    echo '<tr><th scope="row">' . esc_html__('REST API key', 'migratik') . '</th><td>';
    if (migratik_key_is_constant()) {
        echo '<input type="text" class="regular-text code" value="' . esc_attr__('(locked by the MIGRATIK_KEY constant in wp-config.php)', 'migratik') . '" readonly>';
        echo '<p class="description">' . esc_html__('The key is defined in wp-config.php. Remove that constant to manage and rotate the key here.', 'migratik') . '</p>';
    } else {
        echo '<input type="text" class="regular-text code" value="' . esc_attr($settings['rest_key']) . '" readonly onclick="this.select()">';
        echo '<p><label><input type="checkbox" name="' . esc_attr(MIGRATIK_OPTION) . '[regenerate_key]" value="1"> ';
        echo esc_html__('Generate a new key when I save (immediately invalidates the old one).', 'migratik') . '</label></p>';
    }
    echo '<p class="description">' . esc_html__('Guards the REST fallback only: /wp-json/migratik/v1/manifest and /all. The Download button above uses a nonce and never needs this key.', 'migratik') . '</p>';
    echo '</td></tr>';

    echo '</table>';
    submit_button();
    echo '</form>';

    /* translators: %s: plugin version number. */
    echo '<hr style="margin-top:26px"><p class="description">' . sprintf(esc_html__('Migratik v%s — built by Hossam Hamdy.', 'migratik'), esc_html(MIGRATIK_VERSION)) . '</p>';
    echo '</div>';
}

add_action('admin_post_migratik_download', 'migratik_handle_download');
function migratik_handle_download() {
    if (!current_user_can('manage_options')) wp_die(esc_html__('Insufficient permissions.', 'migratik'));
    check_admin_referer('migratik_download');

    $formats = migratik_formats();
    $format  = isset($_POST['format']) ? sanitize_key($_POST['format']) : 'json';
    if (!isset($formats[$format])) $format = 'json';
    $bundle_media = !empty($_POST['bundle_media']);

    // Gate: anything beyond plain JSON is a Pro feature.
    $pro_needed = $formats[$format]['pro'] || $bundle_media;
    if ($pro_needed && !migratik_can_use_pro()) {
        wp_die(esc_html__('That export format is a Migratik Pro feature. Please activate your license to use it.', 'migratik'));
    }

    if (function_exists('wp_raise_memory_limit')) wp_raise_memory_limit('admin');
    // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- large exports can exceed the default max_execution_time
    if (function_exists('set_time_limit')) set_time_limit(0);
    ob_start();
    $data = migratik_build_data();
    // Drop our buffer AND any left open by a page builder (e.g. Elementor's output-buffer
    // callback), so nothing can post-process or inject into the response as we emit it.
    while (ob_get_level()) { ob_end_clean(); }

    /* ---- Simple, free path: plain JSON, no media ---- */
    if ($format === 'json' && !$bundle_media) {
        nocache_headers();
        header('Content-Type: application/json; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . migratik_filename() . '"');
        echo wp_json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    if (!migratik_load_pro_classes()) {
        wp_die(esc_html__('The Pro export modules are missing from this build. Please download Migratik Pro again from your account and reinstall it.', 'migratik'));
    }

    /* ---- Single-file Pro format without media: stream it directly ---- */
    if (!$bundle_media && ($format === 'wxr')) {
        $files = migratik_render_content_files($data, $format);
        $name  = array_key_first($files);
        nocache_headers();
        header('Content-Type: application/xml; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $name . '"');
        echo $files[$name];
        exit;
    }

    /* ---- Everything else -> a ZIP (multi-file formats and/or media) ---- */
    if (!class_exists('ZipArchive')) {
        wp_die(esc_html__('PHP’s ZipArchive extension is required for this export but is not available on this server.', 'migratik'));
    }

    $media_files = [];
    $skipped     = [];
    $nparts      = 1;
    $part        = isset($_POST['part']) ? max(1, (int) $_POST['part']) : 1;

    if ($bundle_media) {
        $plan        = Migratik_Bundle::plan($data);
        $data        = Migratik_Bundle::rewrite($data, $plan['files']); // URLs now point at media/…
        $skipped     = $plan['skipped'];
        $parts       = Migratik_Bundle::split($plan['files'], Migratik_Bundle::limit());
        $nparts      = max(1, count($parts));
        if ($part > $nparts) $part = $nparts;

        // First unqualified click on a large library: offer numbered part buttons.
        if ($nparts > 1 && empty($_POST['confirm_part'])) {
            wp_safe_redirect(add_query_arg(
                ['page' => 'migratik', 'pp_parts' => $nparts, 'pp_format' => $format],
                admin_url('admin.php')
            ));
            exit;
        }
        $media_files = isset($parts[$part - 1]) ? $parts[$part - 1] : [];
    }

    // Content files (rewritten when media is on) go in part 1 only.
    $extra = [];
    if ($part === 1) {
        foreach (migratik_render_content_files($data, $format) as $name => $content) {
            $extra[$name] = $content;
        }
        if ($bundle_media) {
            $map = [];
            foreach ($plan['files'] as $f) $map[$f['url']] = 'media/' . $f['rel'];
            $extra['url-map.json'] = wp_json_encode($map, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
    }
    if ($bundle_media) {
        $extra['README.txt'] = Migratik_Bundle::readme_text($nparts, $part, $part === 1 ? $skipped : [], $format);
    }

    $base  = migratik_export_basename();
    $tag   = $bundle_media ? '-' . $format . '-with-media' : '-' . $format;
    $fname = ($nparts > 1) ? ($base . $tag . '-part-' . $part . '-of-' . $nparts . '.zip') : ($base . $tag . '.zip');

    $tmp = function_exists('wp_tempnam') ? wp_tempnam('migratik') : tempnam(sys_get_temp_dir(), 'ppz');
    if (!$tmp || !Migratik_Bundle::build_zip($tmp, $media_files, $extra)) {
        if ($tmp) @unlink($tmp);
        wp_die(esc_html__('Failed to build the export archive.', 'migratik'));
    }

    nocache_headers();
    header('Content-Type: application/zip');
    header('Content-Disposition: attachment; filename="' . $fname . '"');
    header('Content-Length: ' . filesize($tmp));
    readfile($tmp);
    @unlink($tmp);
    exit;
}

/* -------------------------------------------------------------------------
 * REST fallback (auth: admin capability OR the effective key)
 * ---------------------------------------------------------------------- */
add_action('rest_api_init', function () {
    $auth = function (WP_REST_Request $r) {
        $key = migratik_get_key();
        return ($key !== '' && hash_equals($key, (string) $r->get_param('key'))) || current_user_can('manage_options');
    };
    register_rest_route('migratik/v1', '/manifest', ['methods'=>'GET','permission_callback'=>$auth,'callback'=>function(){ return new WP_REST_Response(migratik_manifest_data(), 200); }]);
    register_rest_route('migratik/v1', '/all',      ['methods'=>'GET','permission_callback'=>$auth,'callback'=>function(){ return new WP_REST_Response(migratik_build_data(), 200); }]);
});
