/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import {
  ControlPanelConfig,
  D3_FORMAT_DOCS,
  D3_FORMAT_OPTIONS,
  getStandardizedControls,
} from '@superset-ui/chart-controls';
import manifest from '../data/manifest.json';
import migrateFromLegacy from './migrateFromLegacy';

// ----------------------------------------------------------------------
// Choice tables — sourced from the build pipeline's manifest.json
//
// The manifest is regenerated by every `./scripts/build.sh` run; adding
// a new worldview or country to the YAML configs and re-running build
// makes the new option appear in the UI without touching this file.
// ----------------------------------------------------------------------

interface CountryMapManifest {
  worldviews: string[];
  admin_levels: number[];
  countries_by_worldview: Record<string, string[]>;
  regional_aggregations: Array<{
    country: string;
    set_id: string;
    worldview: string;
    size_bytes: number;
  }>;
  composites: Array<{
    id: string;
    worldview: string;
    size_bytes: number;
    /** Anchor country (ISO_A3); when set, the composite is only offered
     * while that country is selected. Older manifests omitted this field. */
    country?: string;
  }>;
}

const M = manifest as CountryMapManifest;

// Display-name labels for worldviews. Anything not listed here renders
// as the worldview code itself in the dropdown. The list of *available*
// worldviews is driven by the manifest (= what the build pipeline
// produced), not this map — so a worldview added to scripts/build.py
// and re-built will appear in the dropdown automatically, just with a
// raw code label until you add an entry here.
const WORLDVIEW_LABELS: Record<string, string> = {
  default: t('Natural Earth Default'),
  arg: t('Argentina'),
  bdg: t('Bangladesh'),
  bra: t('Brazil'),
  chn: t('China'),
  deu: t('Germany'),
  egy: t('Egypt'),
  esp: t('Spain'),
  fra: t('France'),
  gbr: t('United Kingdom'),
  grc: t('Greece'),
  idn: t('Indonesia'),
  ind: t('India'),
  iso: t('ISO 3166 (UN-style)'),
  isr: t('Israel'),
  ita: t('Italy'),
  jpn: t('Japan'),
  kor: t('South Korea'),
  mar: t('Morocco'),
  nep: t('Nepal'),
  nld: t('Netherlands'),
  pak: t('Pakistan'),
  pol: t('Poland'),
  prt: t('Portugal'),
  pse: t('Palestine'),
  rus: t('Russia'),
  sau: t('Saudi Arabia'),
  swe: t('Sweden'),
  tur: t('Türkiye'),
  twn: t('Taiwan'),
  ukr: t('Ukraine (Superset default — Crimea as Ukrainian)'),
  usa: t('United States'),
  vnm: t('Vietnam'),
};

const WORLDVIEW_CHOICES: Array<[string, string]> = M.worldviews.map(wv => [
  wv,
  WORLDVIEW_LABELS[wv] || wv,
]);

// Map scope choices. The underlying values stay 0/1/aggregated so saved
// charts and form_data don't break — only the labels change for clarity.
const ADMIN_LEVEL_CHOICES: Array<[string, string]> = [
  [String(0), t('World')],
  [String(1), t('Country')],
  ['aggregated', t('Aggregated regions')],
];

// English country names by ISO_A3, for friendly dropdown labels. Codes
// not listed here render as the raw ISO code (rare; missing entries
// here aren't a correctness problem).
const COUNTRY_LABELS: Record<string, string> = {
  AFG: 'Afghanistan',
  ARG: 'Argentina',
  AUS: 'Australia',
  AUT: 'Austria',
  BEL: 'Belgium',
  BGD: 'Bangladesh',
  BGR: 'Bulgaria',
  BIH: 'Bosnia and Herzegovina',
  BLR: 'Belarus',
  BOL: 'Bolivia',
  BRA: 'Brazil',
  CAN: 'Canada',
  CHE: 'Switzerland',
  CHL: 'Chile',
  CHN: 'China',
  COL: 'Colombia',
  CRI: 'Costa Rica',
  CUB: 'Cuba',
  CYP: 'Cyprus',
  CZE: 'Czechia',
  DEU: 'Germany',
  DNK: 'Denmark',
  DOM: 'Dominican Republic',
  ECU: 'Ecuador',
  EGY: 'Egypt',
  ESP: 'Spain',
  EST: 'Estonia',
  ETH: 'Ethiopia',
  FIN: 'Finland',
  FRA: 'France',
  GBR: 'United Kingdom',
  GHA: 'Ghana',
  GRC: 'Greece',
  GTM: 'Guatemala',
  HND: 'Honduras',
  HRV: 'Croatia',
  HUN: 'Hungary',
  IDN: 'Indonesia',
  IND: 'India',
  IRL: 'Ireland',
  IRN: 'Iran',
  IRQ: 'Iraq',
  ISL: 'Iceland',
  ISR: 'Israel',
  ITA: 'Italy',
  JPN: 'Japan',
  KAZ: 'Kazakhstan',
  KEN: 'Kenya',
  KGZ: 'Kyrgyzstan',
  KHM: 'Cambodia',
  KOR: 'South Korea',
  LAO: 'Laos',
  LBN: 'Lebanon',
  LTU: 'Lithuania',
  LVA: 'Latvia',
  MAR: 'Morocco',
  MEX: 'Mexico',
  MMR: 'Myanmar',
  MNG: 'Mongolia',
  MYS: 'Malaysia',
  NGA: 'Nigeria',
  NLD: 'Netherlands',
  NOR: 'Norway',
  NPL: 'Nepal',
  NZL: 'New Zealand',
  PAK: 'Pakistan',
  PER: 'Peru',
  PHL: 'Philippines',
  POL: 'Poland',
  PRT: 'Portugal',
  PRY: 'Paraguay',
  ROU: 'Romania',
  RUS: 'Russia',
  SAU: 'Saudi Arabia',
  SDN: 'Sudan',
  SRB: 'Serbia',
  SVK: 'Slovakia',
  SVN: 'Slovenia',
  SWE: 'Sweden',
  SYR: 'Syria',
  THA: 'Thailand',
  TUR: 'Türkiye',
  TWN: 'Taiwan',
  UKR: 'Ukraine',
  URY: 'Uruguay',
  USA: 'United States',
  UZB: 'Uzbekistan',
  VEN: 'Venezuela',
  VNM: 'Vietnam',
  YEM: 'Yemen',
  ZAF: 'South Africa',
  ZWE: 'Zimbabwe',
};

const formatCountry = (code: string): string =>
  COUNTRY_LABELS[code] ? `${COUNTRY_LABELS[code]} (${code})` : code;

// Build country choices from the union of all worldview manifests
// (typically the same set since Admin 1 is one global file, but
// future per-worldview Admin 1 outputs would naturally differ).
const COUNTRY_CHOICES: Array<[string, string]> = (() => {
  const all = new Set<string>();
  Object.values(M.countries_by_worldview).forEach(codes =>
    codes.forEach(c => all.add(c)),
  );
  return Array.from(all)
    .sort()
    .map<[string, string]>(c => [c, formatCountry(c)]);
})();

// Region-set labels keyed by `<set_id>` (TUR's `nuts_1` etc.).
const REGION_SET_LABELS: Record<string, string> = {
  nuts_1: t('NUTS-1 statistical regions'),
  regions: t('Administrative regions'),
};

// Build {country: [(set_id, label), ...]} from manifest.
const REGION_SET_CHOICES_BY_COUNTRY: Record<
  string,
  Array<[string, string]>
> = (() => {
  const out: Record<string, Array<[string, string]>> = {};
  M.regional_aggregations.forEach(r => {
    out[r.country] = out[r.country] || [];
    out[r.country].push([r.set_id, REGION_SET_LABELS[r.set_id] || r.set_id]);
  });
  return out;
})();

// Composite-map labels keyed by `<id>`.
const COMPOSITE_LABELS: Record<string, string> = {
  france_overseas: t('France (with overseas territories)'),
};

// Composites grouped by anchor country (ISO_A3). Composites without a
// country in the manifest fall into a "global" bucket and remain
// available regardless of the country selection — keeps backward
// compatibility with older manifests that didn't carry the field.
const COMPOSITES_GLOBAL: Array<[string, string]> = M.composites
  .filter(c => !c.country)
  .map(c => [c.id, COMPOSITE_LABELS[c.id] || c.id]);
const COMPOSITES_BY_COUNTRY: Record<string, Array<[string, string]>> = (() => {
  const out: Record<string, Array<[string, string]>> = {};
  M.composites.forEach(c => {
    if (!c.country) return;
    out[c.country] = out[c.country] || [];
    out[c.country].push([c.id, COMPOSITE_LABELS[c.id] || c.id]);
  });
  return out;
})();
const COUNTRIES_WITH_COMPOSITE = new Set(Object.keys(COMPOSITES_BY_COUNTRY));

const compositeChoicesFor = (
  controls: Record<string, { value?: unknown }>,
): Array<[string, string]> => {
  const country = String(controls.country?.value || '');
  return [
    ...(country && COMPOSITES_BY_COUNTRY[country]
      ? COMPOSITES_BY_COUNTRY[country]
      : []),
    ...COMPOSITES_GLOBAL,
  ];
};

// NE NAME_<lang> language codes available across most features.
const NAME_LANGUAGE_CHOICES: Array<[string, string]> = [
  ['en', t('English (en)')],
  ['fr', t('French (fr)')],
  ['de', t('German (de)')],
  ['es', t('Spanish (es)')],
  ['it', t('Italian (it)')],
  ['pt', t('Portuguese (pt)')],
  ['ru', t('Russian (ru)')],
  ['zh', t('Chinese (zh)')],
  ['ja', t('Japanese (ja)')],
  ['ko', t('Korean (ko)')],
  ['vi', t('Vietnamese (vi)')],
  ['ar', t('Arabic (ar)')],
  ['hi', t('Hindi (hi)')],
  ['fa', t('Persian (fa)')],
  ['tr', t('Turkish (tr)')],
  ['nl', t('Dutch (nl)')],
  ['pl', t('Polish (pl)')],
  ['sv', t('Swedish (sv)')],
  ['el', t('Greek (el)')],
  ['he', t('Hebrew (he)')],
];

// ----------------------------------------------------------------------
// Visibility helpers
// ----------------------------------------------------------------------

const isAdminCountry = (controls: Record<string, { value?: unknown }>) =>
  controls.admin_level?.value === String(0) ||
  controls.admin_level?.value === 0;
const isAdminAggregated = (controls: Record<string, { value?: unknown }>) =>
  controls.admin_level?.value === 'aggregated';
const hasComposite = (controls: Record<string, { value?: unknown }>) =>
  Boolean(controls.composite?.value);

// A country selection is only *required* when the user is rendering
// subdivisions or an aggregated layer AND has not picked a composite.
// At Admin 0 (world choropleth) or with a composite set, country is moot.
const needsCountry = (controls: Record<string, { value?: unknown }>) =>
  !isAdminCountry(controls) && !hasComposite(controls);

const config: ControlPanelConfig = {
  controlPanelSections: [
    {
      label: t('Map'),
      expanded: true,
      controlSetRows: [
        [
          {
            name: 'worldview',
            config: {
              type: 'SelectControl',
              label: t('Worldview'),
              description: t(
                'Cartographic perspective for disputed regions. ' +
                  'Defaults to Ukraine worldview (Crimea shown as Ukrainian); ' +
                  'override per-deployment in superset_config.COUNTRY_MAP.default_worldview.',
              ),
              choices: WORLDVIEW_CHOICES,
              default: 'ukr',
              renderTrigger: true,
              clearable: false,
            },
          },
        ],
        [
          {
            name: 'admin_level',
            config: {
              type: 'SelectControl',
              label: t('Map view'),
              description: t(
                'World shows all countries; Country shows subdivisions of ' +
                  'one country (states/provinces/departments); Aggregated ' +
                  "regions dissolves a country's subdivisions into coarser " +
                  'administrative regions (e.g. French regions, Turkish ' +
                  'NUTS-1 regions). Stored as admin_level (0 / 1 / aggregated).',
              ),
              choices: ADMIN_LEVEL_CHOICES,
              default: String(0),
              renderTrigger: true,
              clearable: false,
            },
          },
        ],
        [
          {
            name: 'country',
            config: {
              type: 'SelectControl',
              label: t('Country'),
              description: t('Which country to plot subdivisions for.'),
              choices: COUNTRY_CHOICES,
              default: null,
              renderTrigger: true,
              clearable: false,
              // Country is only required when the current admin_level /
              // composite combination actually consumes it. Without this,
              // a hidden empty country traps the user with a permanent
              // "Country: cannot be empty" badge on the Data tab even
              // though there's no Country control on that tab.
              mapStateToProps: ({ controls }: any) => ({
                validators: needsCountry(controls) ? [validateNonEmpty] : [],
              }),
              visibility: ({ controls }: any) => needsCountry(controls),
            },
          },
        ],
        [
          {
            name: 'region_set',
            config: {
              type: 'SelectControl',
              label: t('Aggregated region set'),
              description: t(
                'Which administrative region layer to dissolve into. ' +
                  'Available sets depend on the selected country.',
              ),
              default: null,
              renderTrigger: true,
              clearable: true,
              // SelectControl's `choices` must be a literal array, not a
              // function. Use mapStateToProps to derive choices from the
              // currently selected country at render time.
              mapStateToProps: ({ controls }: any) => ({
                choices:
                  REGION_SET_CHOICES_BY_COUNTRY[
                    String(controls.country?.value || '')
                  ] || [],
              }),
              visibility: ({ controls }: any) => isAdminAggregated(controls),
            },
          },
        ],
        [
          {
            name: 'composite',
            config: {
              type: 'SelectControl',
              label: t('Composite map'),
              description: t(
                'Multi-country composite (e.g. France with overseas territories). ' +
                  'Only offered when the selected country has a composite ' +
                  'definition; overrides admin level + country when set.',
              ),
              default: null,
              renderTrigger: true,
              clearable: true,
              // Choices narrow to whatever composites are scoped to the
              // currently selected country, plus any country-agnostic
              // ("global") composites in the manifest.
              mapStateToProps: ({ controls }: any) => ({
                choices: compositeChoicesFor(controls),
              }),
              // Hide when nothing applies: not at the world-map view, not
              // when there are simply no composites in the manifest, and
              // not when the selected country has no scoped composite
              // (and no global composite is available either).
              visibility: ({ controls }: any) => {
                if (isAdminCountry(controls)) return false;
                if (COMPOSITES_GLOBAL.length > 0) return true;
                const country = String(controls.country?.value || '');
                return COUNTRIES_WITH_COMPOSITE.has(country);
              },
            },
          },
        ],
        [
          {
            name: 'region_includes',
            config: {
              type: 'SelectControl',
              multi: true,
              freeForm: true,
              label: t('Include only regions'),
              description: t(
                'Comma-separated ISO codes (iso_3166_2 or adm0_a3). ' +
                  'When set, only these features are rendered. Projection ' +
                  'auto-fits to the included set.',
              ),
              choices: [],
              default: [],
              renderTrigger: true,
            },
          },
        ],
        [
          {
            name: 'region_excludes',
            config: {
              type: 'SelectControl',
              multi: true,
              freeForm: true,
              label: t('Exclude regions'),
              description: t(
                'Comma-separated ISO codes to drop from the rendered map.',
              ),
              choices: [],
              default: [],
              renderTrigger: true,
            },
          },
        ],
        [
          {
            name: 'show_flying_islands',
            config: {
              type: 'CheckboxControl',
              label: t('Show flying islands'),
              description: t(
                'When on (default), territories that the build pipeline ' +
                  'repositioned into insets near the mainland (e.g. ' +
                  'Hawaii/Alaska for US, French DROMs for France) are ' +
                  'visible. Turn off to drop those features entirely; the ' +
                  'projection auto-refits to the remaining mainland.',
              ),
              default: true,
              renderTrigger: true,
            },
          },
        ],
        [
          {
            name: 'name_language',
            config: {
              type: 'SelectControl',
              label: t('Name language'),
              description: t(
                'Which language to use for displayed region names. ' +
                  "Falls back to English when the requested language isn't " +
                  'available for a feature.',
              ),
              choices: NAME_LANGUAGE_CHOICES,
              default: 'en',
              renderTrigger: true,
              clearable: false,
            },
          },
        ],
      ],
    },
    {
      label: t('Query'),
      expanded: true,
      controlSetRows: [
        [
          {
            name: 'entity',
            config: {
              type: 'SelectControl',
              label: t('ISO code column'),
              description: t(
                'Column in your dataset containing ISO codes that match ' +
                  'features in the chosen map (iso_3166_2 for subdivisions, ' +
                  'adm0_a3 for countries).',
              ),
              mapStateToProps: (state: any) => ({
                choices: (state.datasource?.columns ?? []).map((c: any) => [
                  c.column_name,
                  c.column_name,
                ]),
              }),
              validators: [validateNonEmpty],
            },
          },
        ],
        ['metric'],
        ['adhoc_filters'],
        ['row_limit'],
      ],
    },
    {
      label: t('Chart Options'),
      expanded: true,
      tabOverride: 'customize',
      controlSetRows: [
        [
          {
            name: 'number_format',
            config: {
              type: 'SelectControl',
              freeForm: true,
              label: t('Number format'),
              renderTrigger: true,
              default: 'SMART_NUMBER',
              choices: D3_FORMAT_OPTIONS,
              description: D3_FORMAT_DOCS,
            },
          },
        ],
        ['linear_color_scheme'],
      ],
    },
  ],
  controlOverrides: {
    entity: {
      label: t('ISO code column'),
      description: t(
        'Column containing ISO codes of region/province/department in your dataset.',
      ),
    },
    linear_color_scheme: {
      renderTrigger: true,
    },
    // Choropleths key one row per region — even the densest country maps
    // (e.g. France 101 departments, India 36 states, US 51 territories)
    // are well under 10k rows. The shared row_limit default of 50k is
    // both wasteful and exceeds many deployments' configured ROW_LIMIT
    // ceiling, blocking Update Chart with no visible explanation.
    row_limit: {
      default: 10000,
    },
  },
  // formDataOverrides runs when the user switches a chart's viz_type to
  // this plugin. We use it for two jobs:
  //   1. Standard control hand-off (entity, metric) via getStandardizedControls
  //   2. Migration from the legacy country_map plugin — translate the
  //      legacy `select_country` value into admin_level / country /
  //      composite / region_set so the new chart lands pre-populated
  //      rather than dumping the user back to an empty Country dropdown.
  formDataOverrides: formData => {
    const fromLegacy =
      typeof formData.select_country === 'string' && formData.select_country
        ? migrateFromLegacy(formData)
        : {};
    return {
      ...formData,
      entity: getStandardizedControls().shiftColumn(),
      metric: getStandardizedControls().shiftMetric(),
      // Only fill fields the user has not already set on the new chart;
      // explicit user edits on the new viz win over legacy migration.
      ...Object.fromEntries(
        Object.entries(fromLegacy).filter(
          ([k]) => formData[k as keyof typeof formData] == null,
        ),
      ),
    };
  },
};

export default config;
