import React from 'react';

type DistributionItem = {
  label: string;
  count: number;
  percent: number;
  color: string;
};

export class CodeInApplication implements CodeInComp {
  description() {
    return 'Distribution Chart Module - reusable Yeeflow dashboard custom code template for grouped category/count analytics.';
  }

  requiredFields(params?: CodeInParams) {
    return [];
  }

  inputParameters(): InputParameter[] {
    return [
      {
        id: 'dataListId',
        name: 'Data List ID',
        type: 'variable',
        desc: 'Target Yeeflow data list ID used as the chart data source. Supports expression editor values.',
      },
      {
        id: 'categoryField',
        name: 'Category Field',
        type: 'variable',
        desc: 'Field ID/name used to group records, such as Status, Department, Region, Owner, or Category.',
      },
      {
        id: 'titleText',
        name: 'Title Text',
        type: 'string',
        desc: 'Optional module title shown above the chart.',
      },
      {
        id: 'subtitleText',
        name: 'Subtitle Text',
        type: 'string',
        desc: 'Optional helper text shown below the title.',
      },
      {
        id: 'chartType',
        name: 'Chart Type',
        type: 'string',
        desc: 'Chart mode: donut, pie, horizontalBar, bar, or verticalBar. Default is donut.',
      },
      {
        id: 'maxCategories',
        name: 'Max Categories',
        type: 'string',
        desc: 'Maximum number of category groups shown before remaining groups are combined as Other. Default is 8.',
      },
      {
        id: 'showLegend',
        name: 'Show Legend',
        type: 'variable',
        desc: 'Whether the legend/count list is shown. Supports true/false or dynamic expression.',
      },
      {
        id: 'showCount',
        name: 'Show Count',
        type: 'variable',
        desc: 'Whether record counts are shown in chart labels and legend rows. Supports true/false or dynamic expression.',
      },
      {
        id: 'emptyText',
        name: 'Empty Text',
        type: 'string',
        desc: 'Text shown when no records or no category values are available.',
      },
      {
        id: 'colorMode',
        name: 'Color Mode',
        type: 'string',
        desc: 'Color palette: yeeflow, soft, status, or slate. Default is yeeflow.',
      },
      {
        id: 'sortMode',
        name: 'Sort Mode',
        type: 'string',
        desc: 'Sort behavior: countDesc, countAsc, labelAsc, labelDesc, or source. Default is countDesc.',
      },
      {
        id: 'filterExpression',
        name: 'Filter Expression',
        type: 'variable',
        desc: 'Optional Yeeflow query filter object/array/JSON from expression editor. Leave blank for all records.',
      },
      {
        id: 'height',
        name: 'Height',
        type: 'string',
        desc: 'Optional chart area height in pixels. Default is 280.',
      },
      {
        id: 'pageSize',
        name: 'Page Size',
        type: 'string',
        desc: 'Maximum records queried for aggregation. Default is 500.',
      },
      {
        id: 'unknownLabel',
        name: 'Unknown Label',
        type: 'string',
        desc: 'Label used when the category field is empty. Default is Unknown.',
      },
      {
        id: 'otherLabel',
        name: 'Other Label',
        type: 'string',
        desc: 'Label used when categories beyond Max Categories are combined. Default is Other.',
      },
    ];
  }

  render(context: CodeInContext, fieldsValues: any, readonly: boolean) {
    return (
      <DistributionChartModule
        context={context}
        fieldsValues={fieldsValues}
        readonly={readonly}
        params={(context && context.params) || {}}
      />
    );
  }
}

class DistributionChartModule extends React.Component<any, any> {
  private mountedFlag: boolean;

  constructor(props: any) {
    super(props);
    this.mountedFlag = false;
    this.state = {
      loading: false,
      errorText: '',
      items: [],
      totalCount: 0,
    };
  }

  componentDidMount() {
    this.mountedFlag = true;
    this.loadData();
  }

  componentDidUpdate(previousProps: any) {
    if (this.getConfigKey(previousProps.params || {}) !== this.getConfigKey(this.props.params || {})) {
      this.loadData();
    }
  }

  componentWillUnmount() {
    this.mountedFlag = false;
  }

  getConfig() {
    const params = this.props.params || {};
    return {
      dataListId: this.normalizeToString(params.dataListId),
      categoryField: this.normalizeToString(params.categoryField),
      titleText: this.normalizeToString(params.titleText) || 'Distribution',
      subtitleText: this.normalizeToString(params.subtitleText),
      chartType: this.normalizeChartType(this.normalizeToString(params.chartType)),
      maxCategories: Math.max(1, this.normalizeToNumber(params.maxCategories, 8)),
      showLegend: this.normalizeToBoolean(params.showLegend, true),
      showCount: this.normalizeToBoolean(params.showCount, true),
      emptyText: this.normalizeToString(params.emptyText) || 'No distribution data available',
      colorMode: this.normalizeColorMode(this.normalizeToString(params.colorMode)),
      sortMode: this.normalizeSortMode(this.normalizeToString(params.sortMode)),
      filterExpression: this.resolveParameterValue(params.filterExpression),
      height: Math.max(180, this.normalizeToNumber(params.height, 280)),
      pageSize: Math.max(20, this.normalizeToNumber(params.pageSize, 500)),
      unknownLabel: this.normalizeToString(params.unknownLabel) || 'Unknown',
      otherLabel: this.normalizeToString(params.otherLabel) || 'Other',
    };
  }

  getConfigKey(params: any) {
    const config = {
      dataListId: this.normalizeToString(params.dataListId),
      categoryField: this.normalizeToString(params.categoryField),
      chartType: this.normalizeToString(params.chartType),
      maxCategories: this.normalizeToString(params.maxCategories),
      colorMode: this.normalizeToString(params.colorMode),
      sortMode: this.normalizeToString(params.sortMode),
      filterExpression: this.normalizeToString(this.resolveParameterValue(params.filterExpression)),
      pageSize: this.normalizeToString(params.pageSize),
    };
    try {
      return JSON.stringify(config);
    } catch (error) {
      return String(new Date().getTime());
    }
  }

  loadData() {
    const config = this.getConfig();
    if (!config.dataListId || !config.categoryField) {
      this.setState({
        loading: false,
        errorText: '',
        items: [],
        totalCount: 0,
      });
      return;
    }

    const client = this.props.context && this.props.context.modules && this.props.context.modules.yeeSDKClient;
    if (!client || !client.lists || typeof client.lists.queryItems !== 'function') {
      this.setState({
        loading: false,
        errorText: 'Data list service is not available',
        items: [],
        totalCount: 0,
      });
      return;
    }

    this.setState({ loading: true, errorText: '' });
    this.queryListItems(client, config)
      .then((rows: any[]) => {
        const result = this.aggregateRows(rows, config);
        if (this.mountedFlag) {
          this.setState({
            loading: false,
            errorText: '',
            items: result.items,
            totalCount: result.totalCount,
          });
        }
      })
      .catch((error: any) => {
        if (this.mountedFlag) {
          this.setState({
            loading: false,
            errorText: 'Unable to load chart data',
            items: [],
            totalCount: 0,
          });
        }
      });
  }

  queryListItems(client: any, config: any): Promise<any[]> {
    const selectedFields = ['ListDataID', 'ListDataId', config.categoryField];
    const parsedFilter = this.safeParseJson(config.filterExpression);
    const filterValue = parsedFilter !== null && parsedFilter !== undefined ? parsedFilter : config.filterExpression;
    const basePayload: any = {
      listId: config.dataListId,
      dataListId: config.dataListId,
      fields: selectedFields,
      fieldIds: selectedFields,
      selectedFields: selectedFields,
      pageIndex: 1,
      pageNo: 1,
      pageSize: config.pageSize,
    };

    /*
      Yeeflow tenants may expose slightly different queryItems signatures and filter names.
      The module sends common aliases and then extracts rows defensively from the returned shape.
    */
    if (filterValue !== undefined && filterValue !== null && this.normalizeToString(filterValue) !== '') {
      basePayload.filters = filterValue;
      basePayload.filter = filterValue;
      basePayload.where = filterValue;
      basePayload.filterExpression = filterValue;
    }

    const payloadWithoutListId = this.cloneObject(basePayload);
    delete payloadWithoutListId.listId;
    delete payloadWithoutListId.dataListId;

    const attempts = [
      () => client.lists.queryItems(basePayload),
      () => client.lists.queryItems(config.dataListId, payloadWithoutListId),
      () => client.lists.queryItems(config.dataListId, basePayload),
      () => client.lists.queryItems(config.dataListId, payloadWithoutListId, {}),
      () => client.lists.queryItems(config.dataListId, basePayload, {}),
    ];

    return this.tryQueryAttempts(attempts, 0);
  }

  tryQueryAttempts(attempts: any[], index: number): Promise<any[]> {
    if (index >= attempts.length) {
      return Promise.resolve([]);
    }
    let result: any;
    try {
      result = attempts[index]();
    } catch (error) {
      return this.tryQueryAttempts(attempts, index + 1);
    }
    return Promise.resolve(result)
      .then((response: any) => {
        const rows = this.extractRows(response);
        if (rows.length === 0 && index < attempts.length - 1) {
          return this.tryQueryAttempts(attempts, index + 1);
        }
        return rows;
      })
      .catch(() => this.tryQueryAttempts(attempts, index + 1));
  }

  extractRows(response: any): any[] {
    if (!response) {
      return [];
    }
    if (Array.isArray(response)) {
      return response;
    }
    const candidates = [
      response.items,
      response.Items,
      response.data,
      response.Data,
      response.rows,
      response.Rows,
      response.records,
      response.Records,
      response.list,
      response.List,
      response.result,
      response.Result,
      response.value,
      response.Value,
    ];
    for (let index = 0; index < candidates.length; index += 1) {
      if (Array.isArray(candidates[index])) {
        return candidates[index];
      }
      if (candidates[index] && typeof candidates[index] === 'object') {
        const nested = this.extractRows(candidates[index]);
        if (nested.length > 0) {
          return nested;
        }
      }
    }
    return [];
  }

  aggregateRows(rows: any[], config: any) {
    const buckets: any = {};
    const sourceOrder: string[] = [];
    for (let index = 0; index < rows.length; index += 1) {
      const rawValue = this.readItemValue(rows[index], config.categoryField);
      const category = this.normalizeCategory(rawValue, config.unknownLabel);
      if (!buckets[category]) {
        buckets[category] = 0;
        sourceOrder.push(category);
      }
      buckets[category] += 1;
    }

    let items = sourceOrder.map((label) => ({
      label: label,
      count: buckets[label],
      percent: 0,
      color: '',
    }));
    items = this.sortDistributionItems(items, config.sortMode, sourceOrder);
    items = this.limitDistributionItems(items, config.maxCategories, config.otherLabel);

    const totalCount = items.reduce((total, item) => total + item.count, 0);
    const colors = this.getPalette(config.colorMode);
    const finalItems = items.map((item, index) => ({
      label: item.label,
      count: item.count,
      percent: totalCount > 0 ? Math.round((item.count / totalCount) * 1000) / 10 : 0,
      color: colors[index % colors.length],
    }));
    return {
      items: finalItems,
      totalCount: totalCount,
    };
  }

  sortDistributionItems(items: any[], sortMode: string, sourceOrder: string[]) {
    const ordered = items.slice();
    if (sortMode === 'countAsc') {
      return ordered.sort((a, b) => a.count - b.count || a.label.localeCompare(b.label));
    }
    if (sortMode === 'labelAsc') {
      return ordered.sort((a, b) => a.label.localeCompare(b.label));
    }
    if (sortMode === 'labelDesc') {
      return ordered.sort((a, b) => b.label.localeCompare(a.label));
    }
    if (sortMode === 'source') {
      return ordered.sort((a, b) => sourceOrder.indexOf(a.label) - sourceOrder.indexOf(b.label));
    }
    return ordered.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
  }

  limitDistributionItems(items: any[], maxCategories: number, otherLabel: string) {
    if (items.length <= maxCategories) {
      return items;
    }
    const visible = items.slice(0, maxCategories);
    const remaining = items.slice(maxCategories);
    const otherCount = remaining.reduce((total, item) => total + item.count, 0);
    if (otherCount > 0) {
      visible.push({
        label: otherLabel,
        count: otherCount,
        percent: 0,
        color: '',
      });
    }
    return visible;
  }

  readItemValue(item: any, fieldName: string): any {
    if (!item || !fieldName) {
      return undefined;
    }
    const direct = this.readObjectValueByKey(item, fieldName);
    if (direct !== undefined) {
      return this.normalizeCellValue(direct);
    }
    const containers = [
      item.values,
      item.Values,
      item.fields,
      item.Fields,
      item.data,
      item.Data,
      item.item,
      item.Item,
    ];
    for (let index = 0; index < containers.length; index += 1) {
      const value = this.readObjectValueByKey(containers[index], fieldName);
      if (value !== undefined) {
        return this.normalizeCellValue(value);
      }
    }
    const arrays = [item.fieldValues, item.FieldValues, item.fieldsValues, item.FieldsValues];
    for (let groupIndex = 0; groupIndex < arrays.length; groupIndex += 1) {
      const group = arrays[groupIndex];
      if (Array.isArray(group)) {
        for (let index = 0; index < group.length; index += 1) {
          const entry = group[index];
          const entryKey = this.normalizeToString(this.firstDefined([
            entry && entry.fieldId,
            entry && entry.FieldId,
            entry && entry.fieldID,
            entry && entry.FieldID,
            entry && entry.fieldCode,
            entry && entry.FieldCode,
            entry && entry.name,
            entry && entry.Name,
            entry && entry.key,
            entry && entry.Key,
          ]));
          if (entryKey && entryKey.toLowerCase() === fieldName.toLowerCase()) {
            return this.normalizeCellValue(this.firstDefined([
              entry.value,
              entry.Value,
              entry.fieldValue,
              entry.FieldValue,
              entry.dataValue,
              entry.DataValue,
              entry.text,
              entry.Text,
            ]));
          }
        }
      }
    }
    return undefined;
  }

  normalizeCellValue(value: any): any {
    if (value === undefined || value === null) {
      return value;
    }
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
      return value;
    }
    if (Array.isArray(value)) {
      return value.map((item) => this.normalizeCategory(item, '')).filter(Boolean).join(', ');
    }
    if (typeof value === 'object') {
      return this.firstDefined([
        value.value,
        value.Value,
        value.fieldValue,
        value.FieldValue,
        value.dataValue,
        value.DataValue,
        value.valueText,
        value.ValueText,
        value.text,
        value.Text,
        value.display,
        value.Display,
        value.label,
        value.Label,
        value.name,
        value.Name,
        value.title,
        value.Title,
      ]);
    }
    return value;
  }

  normalizeCategory(value: any, fallback: string): string {
    const cell = this.normalizeCellValue(value);
    const text = this.normalizeToString(cell);
    return text || fallback || 'Unknown';
  }

  render() {
    const config = this.getConfig();
    const items: DistributionItem[] = this.state.items || [];
    const hasConfig = Boolean(config.dataListId && config.categoryField);
    const hasData = items.length > 0 && this.state.totalCount > 0;
    return (
      <div className="yf-dist-module">
        <style>{this.getStyles()}</style>
        <div className="yf-dist-header">
          <div>
            <div className="yf-dist-title">{config.titleText}</div>
            {config.subtitleText ? <div className="yf-dist-subtitle">{config.subtitleText}</div> : null}
          </div>
          {hasData ? <div className="yf-dist-total">{this.formatNumber(this.state.totalCount)} total</div> : null}
        </div>

        {!hasConfig ? (
          <div className="yf-dist-empty">Configure Data List ID and Category Field to load the distribution.</div>
        ) : this.state.loading ? (
          <div className="yf-dist-loading">
            <span className="yf-dist-spinner" />
            Loading distribution data
          </div>
        ) : this.state.errorText ? (
          <div className="yf-dist-empty yf-dist-error">{this.state.errorText}</div>
        ) : !hasData ? (
          <div className="yf-dist-empty">{config.emptyText}</div>
        ) : (
          <div className={'yf-dist-body yf-dist-body-' + config.chartType} style={{ minHeight: config.height + 'px' }}>
            <div className="yf-dist-chart">
              {this.renderChart(items, config)}
            </div>
            {config.showLegend ? <div className="yf-dist-legend">{this.renderLegend(items, config)}</div> : null}
          </div>
        )}
      </div>
    );
  }

  renderChart(items: DistributionItem[], config: any) {
    if (config.chartType === 'horizontalBar' || config.chartType === 'bar') {
      return this.renderHorizontalBars(items, config);
    }
    if (config.chartType === 'verticalBar') {
      return this.renderVerticalBars(items, config);
    }
    if (config.chartType === 'pie') {
      return this.renderPie(items, config);
    }
    return this.renderDonut(items, config);
  }

  renderDonut(items: DistributionItem[], config: any) {
    let offset = 25;
    const radius = 42;
    const circumference = 2 * Math.PI * radius;
    const total = items.reduce((sum, item) => sum + item.count, 0);
    return (
      <div className="yf-dist-donut-wrap">
        <svg className="yf-dist-donut" viewBox="0 0 120 120" role="img">
          <circle className="yf-dist-donut-track" cx="60" cy="60" r={radius} />
          {items.map((item, index) => {
            const share = total > 0 ? item.count / total : 0;
            const dash = share * circumference;
            const segment = (
              <circle
                key={item.label + index}
                className="yf-dist-donut-segment"
                cx="60"
                cy="60"
                r={radius}
                stroke={item.color}
                strokeDasharray={dash + ' ' + (circumference - dash)}
                strokeDashoffset={offset}
              />
            );
            offset -= share * 100;
            return segment;
          })}
        </svg>
        <div className="yf-dist-center">
          <div className="yf-dist-center-value">{this.formatNumber(total)}</div>
          <div className="yf-dist-center-label">records</div>
        </div>
      </div>
    );
  }

  renderPie(items: DistributionItem[], config: any) {
    const total = items.reduce((sum, item) => sum + item.count, 0);
    let startAngle = -90;
    return (
      <div className="yf-dist-donut-wrap">
        <svg className="yf-dist-pie" viewBox="0 0 120 120" role="img">
          {items.map((item, index) => {
            const share = total > 0 ? item.count / total : 0;
            const endAngle = startAngle + share * 360;
            const path = this.describeArc(60, 60, 48, startAngle, endAngle);
            startAngle = endAngle;
            return <path key={item.label + index} d={path} fill={item.color} />;
          })}
        </svg>
      </div>
    );
  }

  renderHorizontalBars(items: DistributionItem[], config: any) {
    const maxCount = Math.max.apply(null, items.map((item) => item.count));
    return (
      <div className="yf-dist-bars">
        {items.map((item, index) => {
          const width = maxCount > 0 ? Math.max(3, Math.round((item.count / maxCount) * 100)) : 0;
          return (
            <div className="yf-dist-bar-row" key={item.label + index}>
              <div className="yf-dist-bar-label">{item.label}</div>
              <div className="yf-dist-bar-track">
                <div className="yf-dist-bar-fill" style={{ width: width + '%', backgroundColor: item.color }} />
              </div>
              {config.showCount ? <div className="yf-dist-bar-count">{this.formatNumber(item.count)}</div> : null}
            </div>
          );
        })}
      </div>
    );
  }

  renderVerticalBars(items: DistributionItem[], config: any) {
    const maxCount = Math.max.apply(null, items.map((item) => item.count));
    return (
      <div className="yf-dist-vbars">
        {items.map((item, index) => {
          const height = maxCount > 0 ? Math.max(8, Math.round((item.count / maxCount) * 100)) : 0;
          return (
            <div className="yf-dist-vbar" key={item.label + index}>
              <div className="yf-dist-vbar-meter">
                <div className="yf-dist-vbar-fill" style={{ height: height + '%', backgroundColor: item.color }} />
              </div>
              {config.showCount ? <div className="yf-dist-vbar-count">{this.formatNumber(item.count)}</div> : null}
              <div className="yf-dist-vbar-label" title={item.label}>{item.label}</div>
            </div>
          );
        })}
      </div>
    );
  }

  renderLegend(items: DistributionItem[], config: any) {
    return items.map((item, index) => (
      <div className="yf-dist-legend-row" key={item.label + index}>
        <span className="yf-dist-swatch" style={{ backgroundColor: item.color }} />
        <span className="yf-dist-legend-label" title={item.label}>{item.label}</span>
        <span className="yf-dist-legend-percent">{item.percent}%</span>
        {config.showCount ? <span className="yf-dist-legend-count">{this.formatNumber(item.count)}</span> : null}
      </div>
    ));
  }

  describeArc(cx: number, cy: number, radius: number, startAngle: number, endAngle: number) {
    if (endAngle - startAngle >= 359.99) {
      return 'M ' + cx + ' ' + cy + ' m -' + radius + ', 0 a ' + radius + ',' + radius + ' 0 1,0 ' + (radius * 2) + ',0 a ' + radius + ',' + radius + ' 0 1,0 -' + (radius * 2) + ',0';
    }
    const start = this.polarToCartesian(cx, cy, radius, endAngle);
    const end = this.polarToCartesian(cx, cy, radius, startAngle);
    const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
    return [
      'M', cx, cy,
      'L', start.x, start.y,
      'A', radius, radius, 0, largeArcFlag, 0, end.x, end.y,
      'Z',
    ].join(' ');
  }

  polarToCartesian(cx: number, cy: number, radius: number, angleInDegrees: number) {
    const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
    return {
      x: cx + radius * Math.cos(angleInRadians),
      y: cy + radius * Math.sin(angleInRadians),
    };
  }

  normalizeChartType(value: string) {
    const text = (value || '').toLowerCase();
    if (text === 'pie') {
      return 'pie';
    }
    if (text === 'horizontalbar' || text === 'horizontal-bar' || text === 'bar') {
      return 'horizontalBar';
    }
    if (text === 'verticalbar' || text === 'vertical-bar' || text === 'column') {
      return 'verticalBar';
    }
    return 'donut';
  }

  normalizeSortMode(value: string) {
    const text = (value || '').toLowerCase();
    if (text === 'countasc' || text === 'count-asc') {
      return 'countAsc';
    }
    if (text === 'labelasc' || text === 'label-asc') {
      return 'labelAsc';
    }
    if (text === 'labeldesc' || text === 'label-desc') {
      return 'labelDesc';
    }
    if (text === 'source' || text === 'original') {
      return 'source';
    }
    return 'countDesc';
  }

  normalizeColorMode(value: string) {
    const text = (value || '').toLowerCase();
    if (text === 'soft' || text === 'status' || text === 'slate') {
      return text;
    }
    return 'yeeflow';
  }

  getPalette(colorMode: string) {
    if (colorMode === 'soft') {
      return ['#5B8DEF', '#55BFA3', '#F2B84B', '#EC6F7A', '#8E7CF4', '#4FB3D8', '#A7B1C2', '#6F8FAF'];
    }
    if (colorMode === 'status') {
      return ['#2563EB', '#16A34A', '#F59E0B', '#DC2626', '#7C3AED', '#0891B2', '#64748B', '#0F766E'];
    }
    if (colorMode === 'slate') {
      return ['#315A8C', '#52708C', '#6B879C', '#879CAB', '#9AA7B2', '#AEB7C0', '#6D7D90', '#40556F'];
    }
    return ['#1F6BFF', '#13A3A5', '#F5A623', '#E45665', '#7A5AF8', '#2E90FA', '#667085', '#12B76A'];
  }

  resolveParameterValue(value: any): any {
    if (value === undefined || value === null) {
      return '';
    }
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || Array.isArray(value)) {
      return value;
    }
    if (typeof value === 'object') {
      const preferred = this.firstDefined([
        value.value,
        value.Value,
        value.data,
        value.Data,
        value.result,
        value.Result,
        value.key,
        value.Key,
        value.id,
        value.Id,
        value.fieldId,
        value.FieldId,
        value.fieldName,
        value.FieldName,
        value.label,
        value.Label,
        value.name,
        value.Name,
      ]);
      if (preferred !== undefined) {
        return this.resolveParameterValue(preferred);
      }
    }
    return value;
  }

  safeParseJson(value: any): any {
    if (Array.isArray(value) || (value && typeof value === 'object')) {
      return value;
    }
    const text = this.normalizeToString(value);
    if (!text) {
      return null;
    }
    try {
      return JSON.parse(text);
    } catch (error) {
      return null;
    }
  }

  normalizeToString(value: any): string {
    const resolved = this.resolveParameterValue(value);
    if (resolved === undefined || resolved === null) {
      return '';
    }
    if (typeof resolved === 'string' || typeof resolved === 'number' || typeof resolved === 'boolean') {
      return String(resolved).trim();
    }
    if (Array.isArray(resolved)) {
      return resolved.map((item) => this.normalizeToString(item)).filter(Boolean).join(', ');
    }
    if (typeof resolved === 'object') {
      const preferred = this.firstDefined([
        resolved.value,
        resolved.Value,
        resolved.text,
        resolved.Text,
        resolved.display,
        resolved.Display,
        resolved.label,
        resolved.Label,
        resolved.name,
        resolved.Name,
        resolved.title,
        resolved.Title,
        resolved.key,
        resolved.Key,
      ]);
      if (preferred !== undefined) {
        return this.normalizeToString(preferred);
      }
      try {
        return JSON.stringify(resolved);
      } catch (error) {
        return '';
      }
    }
    return '';
  }

  normalizeToBoolean(value: any, fallback: boolean): boolean {
    const resolved = this.resolveParameterValue(value);
    if (resolved === undefined || resolved === null || resolved === '') {
      return fallback;
    }
    if (typeof resolved === 'boolean') {
      return resolved;
    }
    if (typeof resolved === 'number') {
      return resolved !== 0;
    }
    const text = this.normalizeToString(resolved).toLowerCase();
    if (text === 'true' || text === '1' || text === 'yes' || text === 'y' || text === 'on') {
      return true;
    }
    if (text === 'false' || text === '0' || text === 'no' || text === 'n' || text === 'off') {
      return false;
    }
    return fallback;
  }

  normalizeToNumber(value: any, fallback: number): number {
    const text = this.normalizeToString(value);
    if (!text) {
      return fallback;
    }
    const parsed = Number(text.replace(/,/g, ''));
    return isNaN(parsed) ? fallback : parsed;
  }

  readObjectValueByKey(source: any, wantedKey: string): any {
    if (!source || typeof source !== 'object' || Array.isArray(source) || !wantedKey) {
      return undefined;
    }
    if (source[wantedKey] !== undefined) {
      return source[wantedKey];
    }
    const normalizedWanted = wantedKey.toLowerCase();
    const keys = Object.keys(source);
    for (let index = 0; index < keys.length; index += 1) {
      if (keys[index].toLowerCase() === normalizedWanted) {
        return source[keys[index]];
      }
    }
    return undefined;
  }

  firstDefined(values: any[]) {
    for (let index = 0; index < values.length; index += 1) {
      if (values[index] !== undefined && values[index] !== null) {
        return values[index];
      }
    }
    return undefined;
  }

  cloneObject(value: any) {
    const output: any = {};
    const keys = Object.keys(value || {});
    for (let index = 0; index < keys.length; index += 1) {
      output[keys[index]] = value[keys[index]];
    }
    return output;
  }

  formatNumber(value: number) {
    try {
      return Number(value || 0).toLocaleString();
    } catch (error) {
      return String(value || 0);
    }
  }

  getStyles() {
    return [
      '.yf-dist-module{box-sizing:border-box;width:100%;min-width:0;background:#fff;border:1px solid #e5eaf3;border-radius:8px;padding:18px 18px 16px;color:#111827;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;box-shadow:0 1px 2px rgba(16,24,40,.04);}',
      '.yf-dist-module *{box-sizing:border-box;}',
      '.yf-dist-header{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:16px;}',
      '.yf-dist-title{font-size:16px;line-height:22px;font-weight:700;color:#101828;}',
      '.yf-dist-subtitle{margin-top:3px;font-size:12px;line-height:18px;color:#667085;}',
      '.yf-dist-total{flex:0 0 auto;font-size:12px;line-height:18px;color:#475467;background:#f8fafc;border:1px solid #e5eaf3;border-radius:999px;padding:4px 10px;}',
      '.yf-dist-body{display:grid;grid-template-columns:minmax(180px,1fr) minmax(180px,260px);gap:18px;align-items:center;}',
      '.yf-dist-body-horizontalBar,.yf-dist-body-verticalBar{grid-template-columns:minmax(240px,1fr) minmax(180px,240px);}',
      '.yf-dist-chart{min-width:0;width:100%;display:flex;align-items:center;justify-content:center;}',
      '.yf-dist-donut-wrap{position:relative;width:220px;max-width:100%;aspect-ratio:1/1;display:flex;align-items:center;justify-content:center;}',
      '.yf-dist-donut,.yf-dist-pie{width:100%;height:100%;display:block;}',
      '.yf-dist-donut-track{fill:none;stroke:#eef2f7;stroke-width:14;}',
      '.yf-dist-donut-segment{fill:none;stroke-width:14;stroke-linecap:round;transform:rotate(-90deg);transform-origin:60px 60px;transition:stroke-dasharray .2s ease;}',
      '.yf-dist-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;pointer-events:none;}',
      '.yf-dist-center-value{font-size:26px;line-height:32px;font-weight:750;color:#101828;}',
      '.yf-dist-center-label{font-size:12px;line-height:16px;color:#667085;}',
      '.yf-dist-bars{width:100%;display:flex;flex-direction:column;gap:12px;}',
      '.yf-dist-bar-row{display:grid;grid-template-columns:minmax(84px,150px) minmax(120px,1fr) auto;gap:10px;align-items:center;}',
      '.yf-dist-bar-label{min-width:0;font-size:12px;line-height:16px;color:#344054;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.yf-dist-bar-track{height:10px;background:#eef2f7;border-radius:999px;overflow:hidden;}',
      '.yf-dist-bar-fill{height:100%;border-radius:999px;min-width:3px;}',
      '.yf-dist-bar-count{font-size:12px;line-height:16px;color:#475467;font-variant-numeric:tabular-nums;text-align:right;min-width:34px;}',
      '.yf-dist-vbars{width:100%;min-height:220px;display:flex;align-items:flex-end;justify-content:center;gap:12px;padding:8px 4px 0;}',
      '.yf-dist-vbar{flex:1 1 0;min-width:28px;max-width:58px;display:flex;flex-direction:column;align-items:center;gap:6px;}',
      '.yf-dist-vbar-meter{height:170px;width:100%;border-radius:8px;background:#eef2f7;display:flex;align-items:flex-end;overflow:hidden;}',
      '.yf-dist-vbar-fill{width:100%;border-radius:8px 8px 0 0;}',
      '.yf-dist-vbar-count{font-size:11px;line-height:14px;color:#475467;font-variant-numeric:tabular-nums;}',
      '.yf-dist-vbar-label{width:100%;font-size:11px;line-height:14px;color:#667085;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.yf-dist-legend{display:flex;flex-direction:column;gap:8px;min-width:0;}',
      '.yf-dist-legend-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;gap:8px;align-items:center;min-height:24px;}',
      '.yf-dist-swatch{width:9px;height:9px;border-radius:999px;box-shadow:0 0 0 3px rgba(148,163,184,.12);}',
      '.yf-dist-legend-label{font-size:12px;line-height:16px;color:#344054;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.yf-dist-legend-percent{font-size:12px;line-height:16px;color:#667085;font-variant-numeric:tabular-nums;text-align:right;}',
      '.yf-dist-legend-count{font-size:12px;line-height:16px;font-weight:650;color:#101828;font-variant-numeric:tabular-nums;text-align:right;}',
      '.yf-dist-empty,.yf-dist-loading{min-height:180px;display:flex;align-items:center;justify-content:center;text-align:center;border:1px dashed #d8deea;border-radius:8px;background:#f8fafc;color:#667085;font-size:13px;line-height:20px;padding:24px;}',
      '.yf-dist-error{color:#b42318;background:#fff7f6;border-color:#fecdca;}',
      '.yf-dist-loading{gap:10px;}',
      '.yf-dist-spinner{width:16px;height:16px;border-radius:999px;border:2px solid #d8deea;border-top-color:#1f6bff;animation:yfDistSpin 900ms linear infinite;}',
      '@keyframes yfDistSpin{to{transform:rotate(360deg);}}',
      '@media(max-width:640px){.yf-dist-module{padding:16px;}.yf-dist-header{flex-direction:column;align-items:flex-start;}.yf-dist-body,.yf-dist-body-horizontalBar,.yf-dist-body-verticalBar{grid-template-columns:1fr;gap:16px;}.yf-dist-body{min-height:0!important;}.yf-dist-donut-wrap{width:190px;}.yf-dist-bar-row{grid-template-columns:minmax(74px,120px) minmax(100px,1fr) auto;}.yf-dist-vbars{overflow-x:auto;justify-content:flex-start;padding-bottom:4px;}.yf-dist-vbar{min-width:42px;}.yf-dist-legend{width:100%;}}',
    ].join('');
  }
}
