#!/usr/bin/env bash
#
# Export a WordPress site's configuration and content for Bedrock migration.
#
# Generates imposer state modules, postmark content files, and YooTheme config
# into a specified export directory. All operations are read-only against WordPress.
#
# Usage: bash bin/site-export.sh <export-dir>
#
# Requirements:
#   - WP-CLI available as `wp`
#   - greenleafadvancement/postmark installed (composer require greenleafadvancement/postmark:dev-master)
#   - dirtsimple/imposer installed (composer require dirtsimple/imposer:dev-master)
#
# CiviHosting note: Use ~/private/site-export as the export directory.
# /tmp is not writable on CiviHosting.
#
set -euo pipefail

EXPORT_DIR="${1:?Usage: $0 <export-dir>}"

# Skip plugins that crash WP-CLI:
#   - civicrm: bundled Symfony ServiceLocator incompatible with PHP 8.x Psr\Container
WP="wp --skip-plugins=civicrm"

mkdir -p "$EXPORT_DIR/imposer" \
         "$EXPORT_DIR/content" \
         "$EXPORT_DIR/yootheme"

echo "==> Exporting to: $EXPORT_DIR"

# Record the source domain for use during import (domain rewrite in content)
SOURCE_DOMAIN=$($WP option get siteurl 2>/dev/null | sed 's|https\?://||')
if [ -n "$SOURCE_DOMAIN" ]; then
  echo "$SOURCE_DOMAIN" > "$EXPORT_DIR/.source-domain"
  echo "    Source domain: $SOURCE_DOMAIN"
fi

# ---------------------------------------------------------------------------
# 0. Validate WP-CLI works with our skip list
# ---------------------------------------------------------------------------
echo "==> Verifying WP-CLI connectivity..."
if ! $WP eval "echo 'ok';" 2>&1; then
  echo "ERROR: WP-CLI cannot bootstrap WordPress even with --skip-plugins." >&2
  echo "       Check PHP version compatibility and plugin conflicts." >&2
  exit 1
fi

# ---------------------------------------------------------------------------
# 1. Base options → imposer/base.state.md
# ---------------------------------------------------------------------------
echo "==> Exporting base options..."

# Export base options via wp eval to produce properly-escaped YAML.
# Shell variable interpolation inside heredocs can break YAML when values
# contain special characters (quotes, colons, newlines, backslashes).
$WP eval '
  $opts = [
    "blogname"               => get_option("blogname", ""),
    "blogdescription"        => get_option("blogdescription", ""),
    "timezone_string"        => get_option("timezone_string", "America/New_York"),
    "date_format"            => get_option("date_format", "F j, Y"),
    "time_format"            => get_option("time_format", "g:i a"),
    "permalink_structure"    => get_option("permalink_structure", "/%postname%/"),
    "default_comment_status" => get_option("default_comment_status", "closed"),
    "default_ping_status"    => get_option("default_ping_status", "closed"),
    "show_on_front"          => get_option("show_on_front", "page"),
  ];
  $theme = wp_get_theme()->get_stylesheet();

  // Resolve page_on_front / page_for_posts to slugs for portability
  $front_id = (int) get_option("page_on_front", 0);
  $posts_id = (int) get_option("page_for_posts", 0);
  $front_slug = $front_id ? get_post_field("post_name", $front_id) : "";
  $posts_slug = $posts_id ? get_post_field("post_name", $posts_id) : "";

  // YAML-safe string: always double-quote and escape embedded quotes/backslashes
  $yaml_str = function($v) {
    $v = str_replace("\\", "\\\\", (string) $v);
    $v = str_replace("\"", "\\\"", $v);
    return "\"$v\"";
  };

  $out  = "# Site Base Configuration\n\n";
  $out .= "Exported from source WordPress installation.\n\n";
  $out .= "## Theme\n\n";
  $out .= "```yaml\ntheme: $theme\n```\n\n";
  $out .= "## Core Options\n\n```yaml\noptions:\n";
  foreach ($opts as $key => $val) {
    $out .= "  $key: " . $yaml_str($val) . "\n";
  }
  $out .= "  uploads_use_yearmonth_folders: \"1\"\n";
  $out .= "```\n";
  if ($front_slug) $out .= "\nFront page slug: `$front_slug` (set page_on_front after content import)\n";
  if ($posts_slug) $out .= "Posts page slug: `$posts_slug` (set page_for_posts after content import)\n";

  echo $out;
' 2>/dev/null > "$EXPORT_DIR/imposer/base.state.md"

echo "    imposer/base.state.md"

# ---------------------------------------------------------------------------
# 2. Plugin state → imposer/plugins.state.md
# ---------------------------------------------------------------------------
echo "==> Exporting plugin state..."

{
  echo "# Plugin Configuration"
  echo ""
  echo "Exported from source WordPress installation."
  echo ""
  echo '```yaml'
  echo "plugins:"

  $WP plugin list --format=csv --fields=name,status 2>/dev/null | tail -n +2 | while IFS=, read -r name status; do
    # Skip blank lines or lines without a valid plugin name
    [ -z "$name" ] && continue
    # Map standard WP plugin slugs to Bedrock equivalents
    case "$name" in
      gap-sidebar-main)      name="gap-sidebar" ;;
      jp_yootheme_teritary)  name="jp-yootheme-tertiary" ;;
      wp-meta-seo)           name="wp-meta-seo-addon" ;; # pro replaces free
      imposer-tweaks)        continue ;; # auto-generated by imposer
    esac
    case "$status" in
      active)          echo "  $name: active" ;;
      inactive)        echo "  $name: inactive" ;;
      must-use)        ;; # skip mu-plugins
      dropin)          ;; # skip dropins
      active-network)  echo "  $name: active" ;;
      "")              ;; # skip plugins with empty status (malformed WP-CLI output)
      *)               echo "  # $name: $status" ;;
    esac
  done

  echo '```'
} > "$EXPORT_DIR/imposer/plugins.state.md"

echo "    imposer/plugins.state.md"

# ---------------------------------------------------------------------------
# 3. Theme → imposer/theme.state.md
# ---------------------------------------------------------------------------
echo "==> Exporting theme state..."

active_theme=$($WP theme list --status=active --field=name 2>/dev/null || echo "yootheme")

cat > "$EXPORT_DIR/imposer/theme.state.md" << THEMEEOF
# Theme Configuration

\`\`\`yaml
theme: $active_theme
\`\`\`
THEMEEOF

echo "    imposer/theme.state.md"

# ---------------------------------------------------------------------------
# 4. Menus → imposer/menus.state.md
#    Exports in imposer's native format: menu names, locations, and items
#    with page references by GUID (resolved by imposer via @wp-post after
#    postmark creates the content).
# ---------------------------------------------------------------------------
echo "==> Exporting menus..."

$WP eval '
$locations = get_nav_menu_locations();
$loc_by_menu = [];
foreach ($locations as $loc => $menu_id) {
    if ($menu_id) $loc_by_menu[$menu_id][] = $loc;
}

$menus = wp_get_nav_menus();
if (empty($menus)) { echo "# No menus found\n"; exit; }

// Build item tree per menu
foreach ($menus as $menu) {
    $items = wp_get_nav_menu_items($menu->term_id, ["update_post_term_cache" => false]);
    if (!$items) $items = [];

    // Index by db_id for tree building
    $by_id = [];
    $roots = [];
    foreach ($items as $item) {
        $by_id[$item->db_id] = $item;
        $item->_children = [];
    }
    foreach ($items as $item) {
        if ($item->menu_item_parent && isset($by_id[$item->menu_item_parent])) {
            $by_id[$item->menu_item_parent]->_children[] = $item;
        } else {
            $roots[] = $item;
        }
    }

    $menu_data = new stdClass();
    $menu_data->name = $menu->name;
    $menu_data->locations = $loc_by_menu[$menu->term_id] ?? [];
    $menu_data->items = $roots;
    $menu_data->description = $menu->description;

    $all_menus[] = $menu_data;
}

// YAML output helper
function yaml_indent($depth) { return str_repeat("  ", $depth); }

function yaml_menu_item($item, $depth) {
    $indent = yaml_indent($depth);
    $out = $indent . "-\n";
    $d1 = yaml_indent($depth + 1);

    // Determine type and reference
    switch ($item->type) {
        case "post_type":
            $guid = get_post_field("guid", $item->object_id, "raw");
            $out .= $d1 . "page: " . json_encode($guid, JSON_UNESCAPED_SLASHES) . "\n";
            break;
        case "taxonomy":
            $term = get_term($item->object_id, $item->object);
            if ($term && !is_wp_error($term)) {
                $out .= $d1 . "term:\n";
                $out .= yaml_indent($depth + 2) . $item->object . ": " . json_encode($term->name, JSON_UNESCAPED_SLASHES) . "\n";
            }
            break;
        case "post_type_archive":
            $out .= $d1 . "archive: " . json_encode($item->object, JSON_UNESCAPED_SLASHES) . "\n";
            break;
        case "custom":
        default:
            $out .= $d1 . "url: " . json_encode($item->url, JSON_UNESCAPED_SLASHES) . "\n";
            break;
    }

    // Title (only if custom, otherwise imposer derives from the referenced object)
    if (!empty($item->title)) {
        $out .= $d1 . "title: " . json_encode(html_entity_decode($item->title, ENT_QUOTES, "UTF-8"), JSON_UNESCAPED_SLASHES) . "\n";
    }

    // Optional attributes
    if (!empty($item->description)) $out .= $d1 . "description: " . json_encode($item->description, JSON_UNESCAPED_SLASHES) . "\n";
    if (!empty($item->attr_title))  $out .= $d1 . "attr_title: " . json_encode($item->attr_title, JSON_UNESCAPED_SLASHES) . "\n";
    if (!empty($item->target))      $out .= $d1 . "target: " . json_encode($item->target, JSON_UNESCAPED_SLASHES) . "\n";
    if (!empty($item->xfn))         $out .= $d1 . "xfn: " . json_encode($item->xfn, JSON_UNESCAPED_SLASHES) . "\n";
    $classes = implode(" ", array_filter($item->classes));
    if (!empty($classes))           $out .= $d1 . "classes: " . json_encode($classes, JSON_UNESCAPED_SLASHES) . "\n";

    // Children
    if (!empty($item->_children)) {
        $out .= $d1 . "items:\n";
        foreach ($item->_children as $child) {
            $out .= yaml_menu_item($child, $depth + 2);
        }
    }

    return $out;
}

echo "menus:\n";
foreach ($all_menus as $menu) {
    echo "  " . json_encode($menu->name, JSON_UNESCAPED_SLASHES) . ":\n";
    if (!empty($menu->description)) {
        echo "    description: " . json_encode($menu->description, JSON_UNESCAPED_SLASHES) . "\n";
    }
    if (!empty($menu->locations)) {
        echo "    location:\n";
        foreach ($menu->locations as $loc) {
            echo "      - " . $loc . "\n";
        }
    }
    if (!empty($menu->items)) {
        echo "    items:\n";
        foreach ($menu->items as $item) {
            echo yaml_menu_item($item, 3);
        }
    }
}
' 2>/dev/null > "$EXPORT_DIR/.menus-yaml.tmp"

{
  echo "# Menu Configuration"
  echo ""
  echo "Exported from source WordPress installation."
  echo "Menus are in imposer native format — page items reference posts by GUID."
  echo ""
  echo '```yaml'
  cat "$EXPORT_DIR/.menus-yaml.tmp"
  echo '```'
} > "$EXPORT_DIR/imposer/menus.state.md"

rm -f "$EXPORT_DIR/.menus-yaml.tmp"

echo "    imposer/menus.state.md"

# Export menu item ID → GUID mapping for YooTheme config remapping on import.
# YooTheme stores per-item styling keyed by nav_menu_item db_id which changes
# on every fresh install. This mapping lets the import remap old IDs to new.
echo "==> Exporting menu item ID mapping..."
$WP eval '
$mods = get_option("theme_mods_" . get_option("stylesheet"));
if (!is_array($mods) || empty($mods["config"])) { echo "{}"; exit; }
$cfg = json_decode($mods["config"], true);
$styled_ids = array_keys($cfg["menu"]["items"] ?? []);
if (empty($styled_ids)) { echo "{}"; exit; }

$map = [];
foreach ($styled_ids as $id) {
    $guid = get_post_field("guid", (int)$id, "raw");
    if ($guid) $map[(string)$id] = $guid;
}
echo json_encode($map, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
' 2>/dev/null > "$EXPORT_DIR/yootheme/menu-item-map.json"

if [ -s "$EXPORT_DIR/yootheme/menu-item-map.json" ]; then
  echo "    yootheme/menu-item-map.json"
else
  rm -f "$EXPORT_DIR/yootheme/menu-item-map.json"
fi

# ---------------------------------------------------------------------------
# 5. CiviCRM placeholder → imposer/civicrm.state.md
# ---------------------------------------------------------------------------
echo "==> Creating CiviCRM state placeholder..."

cat > "$EXPORT_DIR/imposer/civicrm.state.md" << CIVIEOF
# CiviCRM Configuration

CiviCRM data is in a separate database and should be migrated via SQL dump/import.
This file is a placeholder for site-specific CiviCRM configuration.
CIVIEOF

echo "    imposer/civicrm.state.md"

# ---------------------------------------------------------------------------
# 5b. ACF definitions (post types, taxonomies, field groups)
#     ACF Pro stores CPTs/taxonomies/field groups as internal post types.
#     Export them as JSON so the target site can register them before content import.
# ---------------------------------------------------------------------------
if $WP plugin is-active advanced-custom-fields-pro 2>/dev/null; then
  echo "==> Exporting ACF definitions..."
  mkdir -p "$EXPORT_DIR/acf"

  for acf_type in acf-post-type acf-taxonomy acf-field-group; do
    ACF_OUT="$(cd "$EXPORT_DIR" && pwd)/acf/${acf_type}.json"

    # ACF Pro 6.1+ has acf_get_raw_internal_post_type_posts for its new internal JSON store,
    # but sites that upgraded without migrating still have field groups as wp_posts entries.
    # Try the modern API first; if it returns nothing, fall back to the legacy wp_posts-based API.
    # Write directly to file via file_put_contents to avoid stdout noise (PHP notices, WP-CLI messages).
    $WP eval "
      \$exported = [];

      // Modern ACF Pro 6.1+ internal JSON store
      if (function_exists('acf_get_raw_internal_post_type_posts')) {
        \$posts = acf_get_raw_internal_post_type_posts('$acf_type');
        foreach (\$posts as \$post) {
          // Ensure fields are loaded — raw posts from unmigrated sites have null fields,
          // which causes acf_prepare_fields_for_export() to fatal.
          if ('$acf_type' === 'acf-field-group' && empty(\$post['fields']) && !empty(\$post['key'])) {
            \$post['fields'] = acf_get_fields(\$post['key']);
          }
          \$prepared = acf_prepare_internal_post_type_for_export(\$post, '$acf_type');
          if (\$prepared) \$exported[] = \$prepared;
        }
      }

      // Fall back to wp_posts-based storage for sites that haven't migrated
      // to ACF's internal JSON store. This covers field groups, post types,
      // and taxonomies stored as custom post type entries in wp_posts.
      if (empty(\$exported)) {
        if ('$acf_type' === 'acf-field-group' && function_exists('acf_get_field_groups')) {
          \$groups = acf_get_field_groups();
          foreach (\$groups as \$group) {
            \$group['fields'] = acf_get_fields(\$group['key']);
            \$exported[] = \$group;
          }
        } elseif (in_array('$acf_type', ['acf-post-type', 'acf-taxonomy'])) {
          // Read directly from wp_posts — these are stored as posts of type acf-post-type / acf-taxonomy
          global \$wpdb;
          \$rows = \$wpdb->get_results(\$wpdb->prepare(
            \"SELECT ID, post_content FROM \$wpdb->posts WHERE post_type = %s AND post_status = 'publish'\",
            '$acf_type'
          ), ARRAY_A);
          foreach (\$rows as \$row) {
            \$data = json_decode(\$row['post_content'], true);
            if (!empty(\$data)) {
              \$exported[] = \$data;
            } else {
              // Some versions store settings in postmeta instead of post_content
              \$meta = get_post_meta(\$row['ID']);
              if (!empty(\$meta)) {
                \$item = [];
                foreach (\$meta as \$k => \$v) {
                  \$item[\$k] = maybe_unserialize(\$v[0]);
                }
                if (!empty(\$item)) \$exported[] = \$item;
              }
            }
          }
        }
      }

      file_put_contents('$ACF_OUT', json_encode(\$exported, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
      fwrite(STDERR, count(\$exported) . ' $acf_type definitions exported' . \"\n\");
    " 2>&1 | grep -v '^Deprecated:\|^Notice:\|^Warning:' || true

    # Validate the output is actually JSON
    if python3 -c "import json, sys; d=json.load(open('$ACF_OUT')); sys.exit(0 if len(d)>0 else 1)" 2>/dev/null; then
      count=$(python3 -c "import json; print(len(json.load(open('$ACF_OUT'))))")
      echo "    acf/${acf_type}.json ($count definitions)"
    else
      # Output wasn't valid JSON or was empty — log and clean up
      if [ -s "$ACF_OUT" ]; then
        content=$(head -5 "$ACF_OUT")
        echo "    WARNING: acf/${acf_type} export produced invalid output: $content" >&2
      fi
      rm -f "$ACF_OUT"
    fi
  done
else
  echo "==> ACF Pro not active, skipping ACF export."
fi

# ---------------------------------------------------------------------------
# 6. Content → content/{type}/ for all public post types
# ---------------------------------------------------------------------------

# Map post type to content directory name (backward compat for page/post)
content_dir_for_type() {
  case "$1" in
    page) echo "pages" ;;
    post) echo "posts" ;;
    *)    echo "$1" ;;
  esac
}

# Built-in types that should not be exported as content
EXCLUDED_TYPES="attachment|revision|nav_menu_item|custom_css|customize_changeset|oembed_cache|user_request|wp_block|wp_template|wp_template_part|wp_global_styles|wp_navigation"

echo "==> Detecting exportable post types..."
POST_TYPES=$($WP post-type list --public=1 --format=csv --fields=name 2>/dev/null | \
  tail -n +2 | grep -v -E "^($EXCLUDED_TYPES)$") || POST_TYPES="page
post"

echo "    Post types: $(echo $POST_TYPES | tr '\n' ' ')"

# Check if postmark is available before attempting content export
has_postmark=false
if $WP cli has-command "postmark export" 2>&1; then
  has_postmark=true
else
  echo "    NOTE: 'wp postmark export' command not found"
fi

# Export each post type
declare -A type_counts
for ptype in $POST_TYPES; do
  dir_name=$(content_dir_for_type "$ptype")
  out_dir="$EXPORT_DIR/content/$dir_name"
  mkdir -p "$out_dir"

  post_ids=$($WP post list --post_type="$ptype" --post_status=any --format=ids 2>&1) || {
    echo "ERROR: Failed to list $ptype:" >&2
    echo "  $post_ids" >&2
    post_ids=""
  }

  if [ -z "$post_ids" ]; then
    type_counts[$ptype]=0
    continue
  fi

  if $has_postmark; then
    echo "==> Exporting $ptype via postmark..."
    count=0
    errors=0
    skipped_slugless=0
    for id in $post_ids; do
      # Skip posts with no slug — postmark requires a slug to generate the filename.
      # These are typically auto-drafts or posts that never had a title set.
      # The "|| true" prevents set -e from aborting on wp-cli errors.
      slug=$($WP post get "$id" --field=post_name 2>/dev/null || true)
      if [ -z "$slug" ]; then
        skipped_slugless=$((skipped_slugless + 1))
        continue
      fi

      if $WP postmark export "$id" --dir="$out_dir/" 2>"$EXPORT_DIR/.postmark-err.tmp"; then
        count=$((count + 1))
      else
        errors=$((errors + 1))
        echo "    WARNING: Failed to export $ptype $id: $(cat "$EXPORT_DIR/.postmark-err.tmp")" >&2
      fi
    done
    rm -f "$EXPORT_DIR/.postmark-err.tmp"
    summary="$count $ptype exported"
    [ "$errors" -gt 0 ] && summary="$summary, $errors failed"
    [ "$skipped_slugless" -gt 0 ] && summary="$summary, $skipped_slugless skipped (no slug)"
    echo "    $summary"
    type_counts[$ptype]=$count
  else
    echo "==> Exporting $ptype via WXR fallback..."
    $WP export --post_type="$ptype" --dir="$out_dir/" \
      && echo "    $ptype exported (WXR format)" \
      || echo "    WARNING: WXR $ptype export failed" >&2
    type_counts[$ptype]="(WXR)"
  fi
done

if ! $has_postmark; then
  echo ""
  echo "    NOTE: WXR export preserves content but postmark format is preferred."
  echo "    Install postmark: composer require greenleafadvancement/postmark"
fi

# ---------------------------------------------------------------------------
# 6c. Media attachments → imposer/attachments.json
#     Exports attachment post records and metadata so the Media Library
#     is populated on the target after uploading files.
# ---------------------------------------------------------------------------
echo "==> Exporting media attachment records..."
ATTACHMENTS_FILE="$(cd "$EXPORT_DIR" && pwd)/imposer/attachments.json"
$WP eval "
  \$attachments = get_posts(['post_type' => 'attachment', 'post_status' => 'any', 'numberposts' => -1]);
  \$export = [];
  foreach (\$attachments as \$att) {
    \$meta = wp_get_attachment_metadata(\$att->ID);
    \$file = get_post_meta(\$att->ID, '_wp_attached_file', true);
    \$alt  = get_post_meta(\$att->ID, '_wp_attachment_image_alt', true);

    // Store parent GUID so the target site can resolve to the new post ID.
    // Source post_parent ID is meaningless on a different site.
    \$parent_guid = '';
    if (\$att->post_parent) {
      \$parent = get_post(\$att->post_parent);
      if (\$parent) \$parent_guid = \$parent->guid;
    }

    \$export[] = [
      'guid'         => \$att->guid,
      'post_title'   => \$att->post_title,
      'post_name'    => \$att->post_name,
      'post_content' => \$att->post_content,
      'post_excerpt' => \$att->post_excerpt,
      'post_status'  => \$att->post_status,
      'post_mime_type'=> \$att->post_mime_type,
      'post_date'    => \$att->post_date,
      'post_date_gmt'=> \$att->post_date_gmt,
      'post_modified' => \$att->post_modified,
      'post_modified_gmt' => \$att->post_modified_gmt,
      'post_parent'  => \$att->post_parent,
      'post_parent_guid' => \$parent_guid,
      '_wp_attached_file' => \$file,
      '_wp_attachment_metadata' => \$meta,
      '_wp_attachment_image_alt' => \$alt,
    ];
  }
  file_put_contents('$ATTACHMENTS_FILE', json_encode(\$export, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
  echo count(\$export) . ' attachments exported' . \"\n\";
" 2>/dev/null
if [ -s "$ATTACHMENTS_FILE" ]; then
  echo "    imposer/attachments.json"
else
  echo "    WARNING: attachment export failed or no attachments found"
fi

# ---------------------------------------------------------------------------
# 6d. Featured image assignments → imposer/featured_images.json
#     Postmark strips _thumbnail_id from exported meta. Save a mapping of
#     post GUID → attachment GUID so the import can reassign them.
# ---------------------------------------------------------------------------
echo "==> Exporting featured image assignments..."
FEATURED_FILE="$(cd "$EXPORT_DIR" && pwd)/imposer/featured_images.json"
$WP eval "
  global \$wpdb;
  \$rows = \$wpdb->get_results(\"
    SELECT p.guid AS post_guid, a.guid AS attachment_guid
    FROM \$wpdb->postmeta pm
    JOIN \$wpdb->posts p ON p.ID = pm.post_id
    JOIN \$wpdb->posts a ON a.ID = pm.meta_value AND a.post_type = 'attachment'
    WHERE pm.meta_key = '_thumbnail_id'
  \", ARRAY_A);
  if (empty(\$rows)) { \$rows = []; }
  file_put_contents('$FEATURED_FILE', json_encode(\$rows, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
  echo count(\$rows) . ' featured image assignments exported' . \"\n\";
" 2>/dev/null
if [ -s "$FEATURED_FILE" ]; then
  echo "    imposer/featured_images.json"
else
  rm -f "$FEATURED_FILE"
  echo "    No featured images found"
fi

# ---------------------------------------------------------------------------
# 6e. ACF attachment references → imposer/acf_attachments.json
#     ACF image/file fields store attachment post IDs. Source IDs are
#     meaningless on the target, so export a mapping of
#     { source_id → attachment_guid } that the import can use to remap.
# ---------------------------------------------------------------------------
echo "==> Exporting ACF attachment field mappings..."
ACF_ATT_FILE="$(cd "$EXPORT_DIR" && pwd)/imposer/acf_attachments.json"
$WP eval "
  global \$wpdb;

  // Collect ACF field keys for image/file/gallery types from registered field groups.
  // These fields store attachment IDs as their meta_value.
  \$acf_keys = [];
  if (function_exists('acf_get_field_groups')) {
    foreach (acf_get_field_groups() as \$group) {
      foreach (acf_get_fields(\$group) as \$field) {
        if (in_array(\$field['type'], ['image', 'file', 'gallery'], true)) {
          \$acf_keys[\$field['name']] = \$field['type'];
        }
      }
    }
  }

  if (empty(\$acf_keys)) {
    echo \"0 ACF attachment fields found\n\";
    file_put_contents('$ACF_ATT_FILE', '{}');
    exit;
  }

  // Build a map of source attachment ID → GUID for all referenced attachments.
  \$id_to_guid = [];
  \$field_names = array_keys(\$acf_keys);
  \$placeholders = implode(',', array_fill(0, count(\$field_names), '%s'));

  \$rows = \$wpdb->get_results(\$wpdb->prepare(
    \"SELECT DISTINCT pm.meta_value
     FROM \$wpdb->postmeta pm
     WHERE pm.meta_key IN (\$placeholders)
       AND pm.meta_value != ''
       AND pm.meta_value REGEXP '^[0-9]+$'\",
    ...\$field_names
  ), ARRAY_A);

  foreach (\$rows as \$row) {
    \$att_id = (int) \$row['meta_value'];
    if (\$att_id <= 0) continue;
    \$att = get_post(\$att_id);
    if (\$att && \$att->post_type === 'attachment') {
      \$id_to_guid[\$att_id] = \$att->guid;
    }
  }

  // Also handle gallery fields (comma-separated or serialized arrays of IDs)
  foreach (\$field_names as \$fname) {
    if (\$acf_keys[\$fname] !== 'gallery') continue;
    \$gallery_rows = \$wpdb->get_results(\$wpdb->prepare(
      \"SELECT pm.meta_value FROM \$wpdb->postmeta pm
       WHERE pm.meta_key = %s AND pm.meta_value != ''\",
      \$fname
    ), ARRAY_A);
    foreach (\$gallery_rows as \$gr) {
      \$val = maybe_unserialize(\$gr['meta_value']);
      if (!is_array(\$val)) continue;
      foreach (\$val as \$aid) {
        \$aid = (int) \$aid;
        if (\$aid <= 0 || isset(\$id_to_guid[\$aid])) continue;
        \$att = get_post(\$aid);
        if (\$att && \$att->post_type === 'attachment') {
          \$id_to_guid[\$aid] = \$att->guid;
        }
      }
    }
  }

  file_put_contents('$ACF_ATT_FILE', json_encode(\$id_to_guid, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
  echo count(\$id_to_guid) . \" ACF attachment references exported\n\";
" 2>/dev/null
if [ -s "$ACF_ATT_FILE" ]; then
  echo "    imposer/acf_attachments.json"
else
  rm -f "$ACF_ATT_FILE"
  echo "    No ACF attachment references found"
fi

# ---------------------------------------------------------------------------
# 6b. Rewrite content paths for Bedrock
# ---------------------------------------------------------------------------
echo "==> Rewriting content paths for Bedrock..."
rewrite_count=0
while IFS= read -r -d '' f; do
  if grep -q 'wp-content/uploads' "$f" 2>/dev/null; then
    sed -i'' -e 's|wp-content/uploads|app/uploads|g' "$f"
    rewrite_count=$((rewrite_count + 1))
  fi
done < <(find "$EXPORT_DIR/content/" -name '*.md' -print0 2>/dev/null)
[ "$rewrite_count" -gt 0 ] && echo "    Rewrote wp-content/uploads → app/uploads in $rewrite_count content files"

# Fix zero dates (unpublished drafts with no real date)
while IFS= read -r -d '' f; do
  if grep -q "0000-00-00 00:00:00" "$f" 2>/dev/null; then
    sed -i'' -e "s/0000-00-00 00:00:00 UTC/2000-01-01 00:00:00 UTC/g" "$f"
    echo "    Fixed zero date in $(basename "$f")"
  fi
done < <(find "$EXPORT_DIR/content/" -name '*.md' -print0 2>/dev/null)

# ---------------------------------------------------------------------------
# 7. YooTheme configuration export
#    Uses wp eval to write clean JSON, avoiding PHP notice pollution from
#    noisy plugins (e.g. wp-meta-seo) that would corrupt wp option get output.
# ---------------------------------------------------------------------------

# Export a WP option as clean JSON via wp eval (immune to stdout noise).
# Handles both array-valued options and options stored as JSON strings
# (e.g., YooTheme stores the 'yootheme' option as a JSON string).
export_option_json() {
  local option="$1" file="$2"
  $WP eval "
    \$val = get_option('$option');
    if (\$val === false) { exit(1); }
    // If the option is already a JSON string, decode it first to avoid double-encoding.
    if (is_string(\$val)) {
      \$decoded = json_decode(\$val, true);
      if (\$decoded !== null) \$val = \$decoded;
    }
    \$json = json_encode(\$val, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
    file_put_contents('$file', \$json);
  " 2>/dev/null
}

echo "==> Exporting YooTheme customizer settings..."
if export_option_json theme_mods_yootheme "$EXPORT_DIR/yootheme/customizer.json"; then
  echo "    yootheme/customizer.json"
else
  echo "    (no YooTheme customizer settings)"
  rm -f "$EXPORT_DIR/yootheme/customizer.json"
fi

# ---------------------------------------------------------------------------
# 7b. YooTheme builder/site config → yootheme/config.json
#     Non-Bedrock sites use the "yootheme" option; Bedrock uses "yootheme_config".
#     Export whichever exists (or both).
# ---------------------------------------------------------------------------
echo "==> Exporting YooTheme builder configuration..."

if export_option_json yootheme_config "$EXPORT_DIR/yootheme/config.json"; then
  echo "    yootheme/config.json"
else
  echo "    (no yootheme_config option)"
  rm -f "$EXPORT_DIR/yootheme/config.json"
fi

if export_option_json yootheme "$EXPORT_DIR/yootheme/yootheme.json"; then
  echo "    yootheme/yootheme.json (layouts, library, templates)"
else
  echo "    (no yootheme option)"
  rm -f "$EXPORT_DIR/yootheme/yootheme.json"
fi

# ---------------------------------------------------------------------------
# 7b2. YooTheme per-page builder content → yootheme/builder_content.json
#      YooTheme stores builder JSON in post_content. Postmark's markdown
#      export strips this, so we export it separately for restoration.
# ---------------------------------------------------------------------------
echo "==> Exporting YooTheme per-page builder content..."
$WP eval '
  global $wpdb;
  $rows = $wpdb->get_results("
    SELECT p.ID, p.guid, p.post_type, p.post_name, p.post_content
    FROM $wpdb->posts p
    WHERE p.post_status IN (\"publish\", \"draft\", \"private\")
    AND p.post_type IN (\"page\", \"post\")
    AND p.post_content LIKE \"{%\"
    AND p.post_content LIKE \"%\\\"children\\\"%\"
  ");
  $entries = [];
  foreach ($rows as $r) {
    $decoded = json_decode($r->post_content, true);
    if ($decoded === null) continue;
    $entries[] = [
      "guid" => $r->guid,
      "post_type" => $r->post_type,
      "post_name" => $r->post_name,
      "builder_content" => $decoded,
    ];
  }
  echo json_encode($entries, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
' 2>/dev/null > "$EXPORT_DIR/yootheme/builder_content.json"

if [ -s "$EXPORT_DIR/yootheme/builder_content.json" ]; then
  COUNT=$(python3 -c "import json; print(len(json.load(open('$EXPORT_DIR/yootheme/builder_content.json'))))" 2>/dev/null || echo "?")
  echo "    yootheme/builder_content.json ($COUNT pages)"
else
  echo "    (no YooTheme builder content found)"
  rm -f "$EXPORT_DIR/yootheme/builder_content.json"
fi

# Rewrite wp-content/uploads → app/uploads for Bedrock path structure
for f in "$EXPORT_DIR/yootheme/"*.json; do
  [ -f "$f" ] || continue
  if grep -q 'wp-content/uploads' "$f" 2>/dev/null; then
    sed -i'' -e 's|wp-content/uploads|app/uploads|g' "$f"
    echo "    (rewrote wp-content/uploads → app/uploads in $(basename "$f"))"
  fi
done

# ---------------------------------------------------------------------------
# 7c. GAP Sidebar config → gap-sidebar/config.yaml
# ---------------------------------------------------------------------------
if $WP plugin is-active gap-sidebar 2>/dev/null; then
  echo "==> Exporting GAP Sidebar configuration..."
  mkdir -p "$EXPORT_DIR/gap-sidebar"
  $WP eval '
    if (!defined("GAP_SIDEBAR_VERSION")) require_once WP_PLUGIN_DIR . "/gap-sidebar/gap-sidebar.php";
    $config = new \GapSidebar\Config();
    $handler = new \GapSidebar\YamlHandler($config);
    echo $handler->export();
  ' > "$EXPORT_DIR/gap-sidebar/config.yaml" 2>/dev/null && {
    echo "    gap-sidebar/config.yaml"
  } || {
    echo "    (gap-sidebar export failed, falling back to raw options)"
    $WP option get gap_sidebar_menu_structure --format=json > "$EXPORT_DIR/gap-sidebar/menu-structure.json" 2>/dev/null
    $WP option get gap_sidebar_colors --format=json > "$EXPORT_DIR/gap-sidebar/colors.json" 2>/dev/null
  }
fi

# ---------------------------------------------------------------------------
# 7d. Widget / sidebar configuration
# ---------------------------------------------------------------------------
echo "==> Exporting widget configuration..."
$WP option get sidebars_widgets --format=json > "$EXPORT_DIR/imposer/sidebars_widgets.json" 2>/dev/null && {
  echo "    imposer/sidebars_widgets.json"
} || {
  echo "    (no widget configuration)"
  rm -f "$EXPORT_DIR/imposer/sidebars_widgets.json"
}

# Export widget instances (search, text, etc.) — sidebars_widgets only stores
# the assignment, not the widget data itself (title, content, etc.)
$WP eval '
global $wpdb;
$rows = $wpdb->get_results(
    "SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE \"widget_%\" AND option_name != \"widget_block\"",
    ARRAY_A
);
$widgets = [];
foreach ($rows as $row) {
    $widgets[$row["option_name"]] = maybe_unserialize($row["option_value"]);
}
echo json_encode($widgets, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
' 2>/dev/null > "$EXPORT_DIR/imposer/widget_instances.json"

if [ -s "$EXPORT_DIR/imposer/widget_instances.json" ]; then
  echo "    imposer/widget_instances.json"
else
  rm -f "$EXPORT_DIR/imposer/widget_instances.json"
fi

# ---------------------------------------------------------------------------
# 7d1. Plugin options (bulk)
#      Exports all wp_options not already handled by other export sections.
#      Sensitive values (passwords, API keys, tokens) are redacted.
#      The resulting JSON is consumed by imposer/plugin-options.state.md.
# ---------------------------------------------------------------------------
echo "==> Exporting plugin options (bulk)..."

PLUGIN_OPTS_FILE="$EXPORT_DIR/imposer/plugin_options.json"
PLUGIN_REDACTED_FILE="$EXPORT_DIR/imposer/plugin_options_redacted.txt"

$WP eval '
global $wpdb;

// Options already exported by other sections — exclude from bulk export
$exclude_exact = [
  // Core WP settings (handled by base.state.md)
  "siteurl", "home", "blogname", "blogdescription", "admin_email",
  "timezone_string", "date_format", "time_format", "permalink_structure",
  "default_comment_status", "default_ping_status", "uploads_use_yearmonth_folders",
  "show_on_front", "page_on_front", "page_for_posts",
  // Core WP internals
  "active_plugins", "template", "stylesheet", "current_theme",
  "db_version", "db_upgraded", "initial_db_version",
  "cron", "rewrite_rules", "recently_activated",
  "uninstall_plugins", "recovery_keys", "wp_force_deactivated_plugins",
  "can_compress_scripts", "finished_updating_comment_type", "fresh_site",
  "WPLANG", "blog_charset", "html_type", "blog_public",
  "default_role", "mailserver_url", "mailserver_login", "mailserver_pass", "mailserver_port",
  "wp_user_roles",
  // Handled by widgets component
  "sidebars_widgets",
  // Handled by yootheme component
  "yootheme", "yootheme_config",
  // Security salts/keys
  "auth_key", "secure_auth_key", "logged_in_key", "nonce_key",
  "auth_salt", "secure_auth_salt", "logged_in_salt", "nonce_salt",
];

// Build the WHERE clause
$placeholders = implode(",", array_fill(0, count($exclude_exact), "%s"));
$where = "option_name NOT IN ($placeholders)";
$params = $exclude_exact;

// LIKE patterns to exclude
$exclude_like = [
  "_transient_%", "_site_transient_%",
  "theme_mods_%",
  "widget_%",
  "gap_sidebar_%",
  "%user_roles",
  "auto_updater.%", "auto_core_update_%",
];
foreach ($exclude_like as $pattern) {
  $where .= " AND option_name NOT LIKE %s";
  $params[] = $pattern;
}

$query = $wpdb->prepare(
  "SELECT option_name, option_value FROM $wpdb->options WHERE $where ORDER BY option_name",
  $params
);
$rows = $wpdb->get_results($query, ARRAY_A);

// Sensitive nested keys — values at these keys will be redacted
$sensitive_keys = [
  "password", "pass", "passwd", "secret", "api_key", "apikey",
  "token", "access_token", "refresh_token",
  "license_key", "license", "activation_key",
  "client_secret", "private_key", "secret_key",
  "site_key", "recaptcha",
];

// Recursive redaction function
function redact_sensitive(&$value, $sensitive_keys, $path, &$redacted_log) {
  if (is_array($value)) {
    foreach ($value as $key => &$v) {
      $lower_key = strtolower((string)$key);
      $is_sensitive = false;
      foreach ($sensitive_keys as $pattern) {
        if (strpos($lower_key, $pattern) !== false) {
          $is_sensitive = true;
          break;
        }
      }
      if ($is_sensitive && !is_array($v) && $v !== "" && $v !== null && $v !== false) {
        $redacted_log[] = $path . "." . $key;
        $v = "__REDACTED__";
      } else {
        redact_sensitive($v, $sensitive_keys, $path . "." . $key, $redacted_log);
      }
    }
    unset($v);
  }
}

$options = [];
$redacted_log = [];

foreach ($rows as $row) {
  $name = $row["option_name"];
  $value = maybe_unserialize($row["option_value"]);

  // If the option is a JSON string, decode it
  if (is_string($value)) {
    $decoded = json_decode($value, true);
    if ($decoded !== null && (is_array($decoded) || is_object($decoded))) {
      $value = $decoded;
    }
  }

  // Redact sensitive nested keys
  redact_sensitive($value, $sensitive_keys, $name, $redacted_log);

  $options[$name] = $value;
}

$json = json_encode($options, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
file_put_contents("'$PLUGIN_OPTS_FILE'", $json);
echo count($options) . " options exported\n";

// Write redaction log
if (!empty($redacted_log)) {
  file_put_contents("'$PLUGIN_REDACTED_FILE'", implode("\n", $redacted_log) . "\n");
  echo count($redacted_log) . " sensitive values redacted (see plugin_options_redacted.txt)\n";
}
' 2>/dev/null

if [ -s "$PLUGIN_OPTS_FILE" ]; then
  echo "    imposer/plugin_options.json"
  [ -s "$PLUGIN_REDACTED_FILE" ] && echo "    imposer/plugin_options_redacted.txt"
else
  echo "    (no plugin options exported)"
  rm -f "$PLUGIN_OPTS_FILE" "$PLUGIN_REDACTED_FILE"
fi

# ---------------------------------------------------------------------------
# 7d2. Roles and capabilities
#      Exports the wp_user_roles option which contains all roles with their
#      capabilities, including custom caps added by plugins (e.g. URE).
# ---------------------------------------------------------------------------
echo "==> Exporting roles and capabilities..."
$WP eval "
  global \$wpdb;
  \$roles = get_option(\$wpdb->get_blog_prefix() . 'user_roles');
  if (\$roles) {
    file_put_contents('$EXPORT_DIR/imposer/user_roles.json',
      json_encode(\$roles, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
    echo '    imposer/user_roles.json' . \"\n\";
  } else {
    echo '    (no roles found)' . \"\n\";
  }
" 2>/dev/null

# ---------------------------------------------------------------------------
# 7e. YooTheme generated assets (compiled CSS, fonts)
# ---------------------------------------------------------------------------
echo "==> Exporting YooTheme generated assets..."
mkdir -p "$EXPORT_DIR/yootheme/generated"

THEME_DIR=$($WP eval 'echo get_template_directory();' 2>/dev/null)
if [ -n "$THEME_DIR" ]; then
  # Check uploads/yootheme first (Bedrock with asset isolation), then theme dir
  UPLOADS_YT="$(dirname "$THEME_DIR")/../uploads/yootheme"
  if [ -d "$UPLOADS_YT/css" ]; then
    ASSET_BASE="$UPLOADS_YT"
  else
    ASSET_BASE="$THEME_DIR"
  fi

  for subdir in css fonts; do
    if [ -d "$ASSET_BASE/$subdir" ]; then
      cp -r "$ASSET_BASE/$subdir" "$EXPORT_DIR/yootheme/generated/"
      echo "    yootheme/generated/$subdir/"
    fi
  done
fi

# ---------------------------------------------------------------------------
# 8. Users → imposer/users.state.md
# ---------------------------------------------------------------------------
echo "==> Exporting users..."

# Use wp eval to export users as YAML, avoiding CSV comma-in-roles parsing issues.
# wp user list --format=csv breaks when roles contain commas (e.g. "cms_user,crm_admin").
$WP eval '
  $users = get_users(["orderby" => "ID", "order" => "ASC"]);
  $out  = "# User Accounts\n\n";
  $out .= "Exported from source WordPress installation.\n";
  $out .= "Users authenticate via Greenleaf OIDC — passwords are not migrated.\n\n";
  $out .= "```yaml\nusers:\n";
  foreach ($users as $u) {
    $login = $u->user_login;
    $email = $u->user_email;
    // Take the first role (primary)
    $role = !empty($u->roles) ? reset($u->roles) : "subscriber";
    // Escape for YAML double-quoted strings
    $esc = function($v) {
      $v = str_replace("\\", "\\\\", (string) $v);
      $v = str_replace("\"", "\\\"", $v);
      return "\"$v\"";
    };
    $out .= "  - login: " . $esc($login) . "\n";
    $out .= "    email: " . $esc($email) . "\n";
    $out .= "    role: " . $esc($role) . "\n";
  }
  $out .= "```\n";
  echo $out;
' 2>/dev/null > "$EXPORT_DIR/imposer/users.state.md"

echo "    imposer/users.state.md"

# ---------------------------------------------------------------------------
# 9. Redirection rules (if plugin is active)
# ---------------------------------------------------------------------------
if $WP plugin is-active redirection 2>/dev/null; then
  echo "==> Exporting redirection rules..."
  REDIR_FILE="$(cd "$EXPORT_DIR" && pwd)/imposer/redirections.json"
  # The WP-CLI command signature is: wp redirection export <module> <filename> [--format=<format>]
  # Write directly to the target file (not stdout) to avoid WP-CLI success messages in the output.
  if $WP redirection export all "$REDIR_FILE" --format=json 2>/dev/null; then
    # Strip any leading non-JSON content (PHP notices, WP-CLI messages) from the file
    if [ -s "$REDIR_FILE" ]; then
      python3 -c "
import json, sys
raw = open('$REDIR_FILE').read()
# Find the first [ or { to skip leading noise
for i, c in enumerate(raw):
    if c in '[{':
        try:
            data = json.loads(raw[i:])
            with open('$REDIR_FILE', 'w') as f:
                json.dump(data, f, indent=2)
            sys.exit(0)
        except json.JSONDecodeError:
            continue
sys.exit(1)
" 2>/dev/null
      if [ $? -eq 0 ]; then
        echo "    imposer/redirections.json"
      else
        echo "    WARNING: redirection export produced non-JSON output, skipping" >&2
        rm -f "$REDIR_FILE"
      fi
    else
      echo "    WARNING: redirection export produced empty file, skipping" >&2
      rm -f "$REDIR_FILE"
    fi
  else
    echo "    (wp redirection export not available, skip)"
  fi
fi

# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo "=== Export complete ==="
echo ""
echo "Export directory: $EXPORT_DIR"
echo "  imposer/     - State modules ($(ls "$EXPORT_DIR/imposer/"*.state.md 2>/dev/null | wc -l | tr -d ' ') files)"
content_summary=""
for ptype in $POST_TYPES; do
  cnt="${type_counts[$ptype]:-0}"
  content_summary="${content_summary:+$content_summary, }$ptype: $cnt"
done
echo "  content/     - $content_summary"
yootheme_files=""
test -f "$EXPORT_DIR/yootheme/customizer.json" && yootheme_files="customizer.json"
test -f "$EXPORT_DIR/yootheme/config.json" && yootheme_files="${yootheme_files:+$yootheme_files, }config.json"
test -f "$EXPORT_DIR/yootheme/yootheme.json" && yootheme_files="${yootheme_files:+$yootheme_files, }yootheme.json"
echo "  yootheme/    - ${yootheme_files:-(none)}"
sidebar_files=""
test -f "$EXPORT_DIR/gap-sidebar/config.yaml" && sidebar_files="config.yaml"
test -f "$EXPORT_DIR/gap-sidebar/menu-structure.json" && sidebar_files="${sidebar_files:+$sidebar_files, }menu-structure.json, colors.json"
test -n "$sidebar_files" && echo "  gap-sidebar/ - $sidebar_files"
test -d "$EXPORT_DIR/yootheme/generated/css" && echo "  yootheme/generated/ - css/, fonts/"
echo ""
echo "Next steps:"
echo "  1. Copy exports to your Bedrock project"
echo "  2. rsync uploads: rsync -avz wp-content/uploads/ <bedrock>/web/app/uploads/"
echo "  3. On target: TARGET_DOMAIN=new.domain.com bash bin/imposer-apply.sh"
