import React from 'react';

type TrendPoint = {
  key: string;
  label: string;
  count: number;
  value: number;
  x: number;
  y: number;
};

export class CodeInApplication implements CodeInComp {
  description() {
    return 'Trend Chart Module - reusable Yeeflow dashboard custom code template for time-based list trends.';
  }

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

  inputParameters(): InputParameter[] {
    return [
      {
        id: 'dataListId',
        name: 'Data List ID',
        type: 'variable',
        desc: 'Target Yeeflow data list ID used as the trend data source. Supports expression editor values.',
      },
      {
        id: 'dateField',
        name: 'Date Field',
        type: 'variable',
        desc: 'Field ID/name used as the time axis, such as CreatedTime, SubmitDate, CompletedDate, or AttendanceDate.',
      },
      {
        id: 'titleText',
        name: 'Title Text',
        type: 'string',
        desc: 'Optional module title shown above the chart.',
      },
      {
        id: 'subtitleText',
        name: 'Subtitle Text',
        type: 'string',
        desc: 'Optional supporting text shown below the title.',
      },
      {
        id: 'chartType',
        name: 'Chart Type',
        type: 'string',
        desc: 'Chart mode: line, area, areaLine, column, or bar. Default is line.',
      },
      {
        id: 'timeGranularity',
        name: 'Time Granularity',
        type: 'string',
        desc: 'Time bucket grouping: day, week, or month. Default is day.',
      },
      {
        id: 'maxPoints',
        name: 'Max Points',
        type: 'string',
        desc: 'Maximum number of time buckets shown. Default is 12.',
      },
      {
        id: 'showPointLabels',
        name: 'Show Point Labels',
        type: 'variable',
        desc: 'Whether value labels are shown on points or columns. Supports true/false or dynamic expression.',
      },
      {
        id: 'showXAxisLabels',
        name: 'Show X Axis Labels',
        type: 'variable',
        desc: 'Whether date bucket labels are shown on the x-axis. Supports true/false or dynamic expression.',
      },
      {
        id: 'showYAxis',
        name: 'Show Y Axis',
        type: 'variable',
        desc: 'Whether a simple y-axis and grid are shown. Supports true/false or dynamic expression.',
      },
      {
        id: 'emptyText',
        name: 'Empty Text',
        type: 'string',
        desc: 'Text shown when no valid dated records are available.',
      },
      {
        id: 'colorMode',
        name: 'Color Mode',
        type: 'string',
        desc: 'Color palette: yeeflow, green, amber, red, violet, or slate. Default is yeeflow.',
      },
      {
        id: 'sortMode',
        name: 'Sort Mode',
        type: 'string',
        desc: 'Time order: chronological or reverseChronological. Default is chronological.',
      },
      {
        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: 'dateRangeMode',
        name: 'Date Range Mode',
        type: 'string',
        desc: 'Optional local date range: all, last7Days, last30Days, last90Days, thisMonth, or last12Months. Default is all.',
      },
      {
        id: 'cumulativeMode',
        name: 'Cumulative Mode',
        type: 'variable',
        desc: 'Whether the chart shows running total values instead of per-bucket counts. Supports true/false or dynamic expression.',
      },
      {
        id: 'fillMissingBuckets',
        name: 'Fill Missing Buckets',
        type: 'variable',
        desc: 'Whether missing day/week/month buckets should be shown as zero. Supports true/false or dynamic expression.',
      },
      {
        id: 'unknownDateLabel',
        name: 'Unknown Date Label',
        type: 'string',
        desc: 'Internal label used for invalid dates in diagnostics. Invalid dates are excluded from the chart.',
      },
    ];
  }

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

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

  constructor(props: any) {
    super(props);
    this.mountedFlag = false;
    this.state = {
      loading: false,
      errorText: '',
      points: [],
      totalCount: 0,
      invalidDateCount: 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),
      dateField: this.normalizeToString(params.dateField),
      titleText: this.normalizeToString(params.titleText) || 'Trend',
      subtitleText: this.normalizeToString(params.subtitleText),
      chartType: this.normalizeChartType(this.normalizeToString(params.chartType)),
      timeGranularity: this.normalizeGranularity(this.normalizeToString(params.timeGranularity)),
      maxPoints: Math.max(2, this.normalizeToNumber(params.maxPoints, 12)),
      showPointLabels: this.normalizeToBoolean(params.showPointLabels, false),
      showXAxisLabels: this.normalizeToBoolean(params.showXAxisLabels, true),
      showYAxis: this.normalizeToBoolean(params.showYAxis, true),
      emptyText: this.normalizeToString(params.emptyText) || 'No trend data available',
      colorMode: this.normalizeColorMode(this.normalizeToString(params.colorMode)),
      sortMode: this.normalizeSortMode(this.normalizeToString(params.sortMode)),
      filterExpression: this.resolveParameterValue(params.filterExpression),
      height: Math.max(200, this.normalizeToNumber(params.height, 280)),
      pageSize: Math.max(20, this.normalizeToNumber(params.pageSize, 500)),
      dateRangeMode: this.normalizeDateRangeMode(this.normalizeToString(params.dateRangeMode)),
      cumulativeMode: this.normalizeToBoolean(params.cumulativeMode, false),
      fillMissingBuckets: this.normalizeToBoolean(params.fillMissingBuckets, true),
      unknownDateLabel: this.normalizeToString(params.unknownDateLabel) || 'Invalid date',
    };
  }

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

  loadData() {
    const config = this.getConfig();
    if (!config.dataListId || !config.dateField) {
      this.setState({
        loading: false,
        errorText: '',
        points: [],
        totalCount: 0,
        invalidDateCount: 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',
        points: [],
        totalCount: 0,
        invalidDateCount: 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: '',
            points: result.points,
            totalCount: result.totalCount,
            invalidDateCount: result.invalidDateCount,
          });
        }
      })
      .catch(() => {
        if (this.mountedFlag) {
          this.setState({
            loading: false,
            errorText: 'Unable to load trend data',
            points: [],
            totalCount: 0,
            invalidDateCount: 0,
          });
        }
      });
  }

  queryListItems(client: any, config: any): Promise<any[]> {
    const selectedFields = ['ListDataID', 'ListDataId', config.dateField];
    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 list-query filter support can vary between runtimes.
      This module sends common aliases and then performs date bucketing locally.
    */
    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 range = this.getDateRange(config.dateRangeMode);
    let invalidDateCount = 0;
    let totalCount = 0;

    for (let index = 0; index < rows.length; index += 1) {
      const rawValue = this.readItemValue(rows[index], config.dateField);
      const date = this.parseDateValue(rawValue);
      if (!date) {
        invalidDateCount += 1;
      } else if (this.isDateInRange(date, range)) {
        const bucket = this.getBucket(date, config.timeGranularity);
        if (!buckets[bucket.key]) {
          buckets[bucket.key] = {
            key: bucket.key,
            label: bucket.label,
            date: bucket.date,
            count: 0,
          };
        }
        buckets[bucket.key].count += 1;
        totalCount += 1;
      }
    }

    let rawPoints = Object.keys(buckets).map((key) => buckets[key]);
    rawPoints = this.sortBucketPoints(rawPoints, 'chronological');
    if (config.fillMissingBuckets && rawPoints.length > 0) {
      rawPoints = this.fillMissingBuckets(rawPoints, config.timeGranularity);
    }
    rawPoints = this.limitTrendPoints(rawPoints, config.maxPoints);

    let runningTotal = 0;
    let points = rawPoints.map((item) => {
      runningTotal += item.count;
      return {
        key: item.key,
        label: item.label,
        count: item.count,
        value: config.cumulativeMode ? runningTotal : item.count,
        x: 0,
        y: 0,
      };
    });
    if (config.sortMode === 'reverseChronological') {
      points = points.slice().reverse();
    }

    return {
      points: points,
      totalCount: totalCount,
      invalidDateCount: invalidDateCount,
    };
  }

  sortBucketPoints(points: any[], sortMode: string) {
    const ordered = points.slice();
    ordered.sort((a, b) => {
      const aTime = a.date ? a.date.getTime() : 0;
      const bTime = b.date ? b.date.getTime() : 0;
      return aTime - bTime;
    });
    if (sortMode === 'reverseChronological') {
      return ordered.reverse();
    }
    return ordered;
  }

  limitTrendPoints(points: any[], maxPoints: number) {
    if (points.length <= maxPoints) {
      return points;
    }
    return points.slice(points.length - maxPoints);
  }

  fillMissingBuckets(points: any[], granularity: string) {
    if (!points || points.length < 2) {
      return points || [];
    }
    const output: any[] = [];
    let cursor = this.cloneDate(points[0].date);
    const last = points[points.length - 1].date;
    const bucketMap: any = {};
    for (let index = 0; index < points.length; index += 1) {
      bucketMap[points[index].key] = points[index];
    }
    let guard = 0;
    while (cursor && last && cursor.getTime() <= last.getTime() && guard < 500) {
      const bucket = this.getBucket(cursor, granularity);
      output.push(bucketMap[bucket.key] || {
        key: bucket.key,
        label: bucket.label,
        date: bucket.date,
        count: 0,
      });
      cursor = this.addBucket(cursor, granularity);
      guard += 1;
    }
    return output;
  }

  getBucket(date: Date, granularity: string) {
    if (granularity === 'month') {
      const bucketDate = new Date(date.getFullYear(), date.getMonth(), 1);
      return {
        key: this.formatDateKey(bucketDate, 'month'),
        label: this.formatMonthLabel(bucketDate),
        date: bucketDate,
      };
    }
    if (granularity === 'week') {
      const bucketDate = this.getWeekStart(date);
      return {
        key: this.formatDateKey(bucketDate, 'day'),
        label: this.formatShortDate(bucketDate),
        date: bucketDate,
      };
    }
    const bucketDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
    return {
      key: this.formatDateKey(bucketDate, 'day'),
      label: this.formatShortDate(bucketDate),
      date: bucketDate,
    };
  }

  addBucket(date: Date, granularity: string) {
    const next = this.cloneDate(date);
    if (granularity === 'month') {
      next.setMonth(next.getMonth() + 1);
    } else if (granularity === 'week') {
      next.setDate(next.getDate() + 7);
    } else {
      next.setDate(next.getDate() + 1);
    }
    return next;
  }

  getWeekStart(date: Date) {
    const weekStart = new Date(date.getFullYear(), date.getMonth(), date.getDate());
    const day = weekStart.getDay();
    const diff = day === 0 ? -6 : 1 - day;
    weekStart.setDate(weekStart.getDate() + diff);
    return weekStart;
  }

  getDateRange(mode: string) {
    const today = new Date();
    const end = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999);
    let start: Date | null = null;
    if (mode === 'last7Days') {
      start = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6);
    } else if (mode === 'last30Days') {
      start = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29);
    } else if (mode === 'last90Days') {
      start = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 89);
    } else if (mode === 'thisMonth') {
      start = new Date(today.getFullYear(), today.getMonth(), 1);
    } else if (mode === 'last12Months') {
      start = new Date(today.getFullYear(), today.getMonth() - 11, 1);
    }
    return {
      start: start,
      end: mode === 'all' ? null : end,
    };
  }

  isDateInRange(date: Date, range: any) {
    if (!date) {
      return false;
    }
    if (range && range.start && date.getTime() < range.start.getTime()) {
      return false;
    }
    if (range && range.end && date.getTime() > range.end.getTime()) {
      return false;
    }
    return true;
  }

  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 (value instanceof Date) {
      return value;
    }
    if (Array.isArray(value)) {
      return value.length > 0 ? this.normalizeCellValue(value[0]) : undefined;
    }
    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;
  }

  parseDateValue(value: any): Date | null {
    const cell = this.normalizeCellValue(value);
    if (cell === undefined || cell === null || cell === '') {
      return null;
    }
    if (cell instanceof Date && !isNaN(cell.getTime())) {
      return cell;
    }
    if (typeof cell === 'number') {
      if (cell > 100000000000) {
        const msDate = new Date(cell);
        return isNaN(msDate.getTime()) ? null : msDate;
      }
      if (cell > 1000000000) {
        const secDate = new Date(cell * 1000);
        return isNaN(secDate.getTime()) ? null : secDate;
      }
      if (cell > 20000 && cell < 80000) {
        const excelDate = new Date(Math.round((cell - 25569) * 86400 * 1000));
        return isNaN(excelDate.getTime()) ? null : excelDate;
      }
    }
    const text = this.normalizeToString(cell);
    if (!text) {
      return null;
    }
    const simpleDate = /^(\d{4})-(\d{1,2})-(\d{1,2})/.exec(text);
    if (simpleDate) {
      return new Date(Number(simpleDate[1]), Number(simpleDate[2]) - 1, Number(simpleDate[3]));
    }
    const slashDate = /^(\d{1,2})\/(\d{1,2})\/(\d{4})/.exec(text);
    if (slashDate) {
      return new Date(Number(slashDate[3]), Number(slashDate[1]) - 1, Number(slashDate[2]));
    }
    const parsed = new Date(text);
    return isNaN(parsed.getTime()) ? null : parsed;
  }

  render() {
    const config = this.getConfig();
    const points: TrendPoint[] = this.state.points || [];
    const hasConfig = Boolean(config.dataListId && config.dateField);
    const hasData = points.length > 0 && this.state.totalCount > 0;
    const valueLabel = config.cumulativeMode ? 'cumulative' : 'total';
    return (
      <div className="yf-trend-module">
        <style>{this.getStyles()}</style>
        <div className="yf-trend-header">
          <div>
            <div className="yf-trend-title">{config.titleText}</div>
            {config.subtitleText ? <div className="yf-trend-subtitle">{config.subtitleText}</div> : null}
          </div>
          {hasData ? (
            <div className="yf-trend-summary">
              <span>{this.formatNumber(this.state.totalCount)}</span>
              <em>{valueLabel}</em>
            </div>
          ) : null}
        </div>

        {!hasConfig ? (
          <div className="yf-trend-empty">Configure Data List ID and Date Field to load the trend.</div>
        ) : this.state.loading ? (
          <div className="yf-trend-loading">
            <span className="yf-trend-spinner" />
            Loading trend data
          </div>
        ) : this.state.errorText ? (
          <div className="yf-trend-empty yf-trend-error">{this.state.errorText}</div>
        ) : !hasData ? (
          <div className="yf-trend-empty">{config.emptyText}</div>
        ) : (
          <div className="yf-trend-body" style={{ minHeight: config.height + 'px' }}>
            {this.renderChart(points, config)}
            {this.state.invalidDateCount > 0 ? (
              <div className="yf-trend-note">
                {this.formatNumber(this.state.invalidDateCount)} records skipped because the date value was empty or invalid.
              </div>
            ) : null}
          </div>
        )}
      </div>
    );
  }

  renderChart(points: TrendPoint[], config: any) {
    const chartWidth = 640;
    const chartHeight = 250;
    const margin = {
      top: config.showPointLabels ? 26 : 16,
      right: 18,
      bottom: config.showXAxisLabels ? 46 : 18,
      left: config.showYAxis ? 48 : 18,
    };
    const plotWidth = chartWidth - margin.left - margin.right;
    const plotHeight = chartHeight - margin.top - margin.bottom;
    const maxValue = Math.max(1, Math.max.apply(null, points.map((point) => point.value)));
    const color = this.getPalette(config.colorMode);
    const plotted = points.map((point, index) => {
      const denominator = points.length > 1 ? points.length - 1 : 1;
      const x = margin.left + (plotWidth * index) / denominator;
      const y = margin.top + plotHeight - (point.value / maxValue) * plotHeight;
      return {
        key: point.key,
        label: point.label,
        count: point.count,
        value: point.value,
        x: x,
        y: y,
      };
    });
    const linePath = this.buildLinePath(plotted);
    const areaPath = this.buildAreaPath(plotted, margin.top + plotHeight);

    return (
      <div className="yf-trend-chart-wrap">
        <svg className="yf-trend-chart" viewBox={'0 0 ' + chartWidth + ' ' + chartHeight} preserveAspectRatio="none" role="img">
          {config.showYAxis ? this.renderGrid(chartWidth, margin, plotHeight, maxValue) : null}
          {config.chartType === 'column' ? this.renderColumns(plotted, margin, plotWidth, plotHeight, color, config) : null}
          {config.chartType === 'area' ? <path className="yf-trend-area" d={areaPath} fill={color.area} /> : null}
          {config.chartType !== 'column' ? <path className="yf-trend-line" d={linePath} stroke={color.main} /> : null}
          {config.chartType !== 'column' ? this.renderPoints(plotted, color, config) : null}
          {config.showXAxisLabels ? this.renderXAxisLabels(plotted, chartHeight) : null}
        </svg>
      </div>
    );
  }

  renderGrid(chartWidth: number, margin: any, plotHeight: number, maxValue: number) {
    const ticks = [0, 0.5, 1];
    return (
      <g>
        {ticks.map((tick, index) => {
          const y = margin.top + plotHeight - plotHeight * tick;
          const value = Math.round(maxValue * tick);
          return (
            <g key={'grid-' + index}>
              <line className="yf-trend-grid" x1={margin.left} x2={chartWidth - margin.right} y1={y} y2={y} />
              <text className="yf-trend-y-label" x={margin.left - 10} y={y + 4}>{this.formatNumber(value)}</text>
            </g>
          );
        })}
      </g>
    );
  }

  renderColumns(points: TrendPoint[], margin: any, plotWidth: number, plotHeight: number, color: any, config: any) {
    const columnGap = points.length > 8 ? 5 : 10;
    const columnWidth = Math.max(8, Math.min(42, plotWidth / Math.max(points.length, 1) - columnGap));
    const baseline = margin.top + plotHeight;
    return (
      <g>
        {points.map((point, index) => {
          const barHeight = Math.max(2, baseline - point.y);
          return (
            <g key={'column-' + point.key + index}>
              <rect
                className="yf-trend-column"
                x={point.x - columnWidth / 2}
                y={point.y}
                width={columnWidth}
                height={barHeight}
                rx="5"
                fill={color.main}
              />
              {config.showPointLabels ? (
                <text className="yf-trend-point-label" x={point.x} y={point.y - 8}>{this.formatNumber(point.value)}</text>
              ) : null}
            </g>
          );
        })}
      </g>
    );
  }

  renderPoints(points: TrendPoint[], color: any, config: any) {
    return (
      <g>
        {points.map((point, index) => (
          <g key={'point-' + point.key + index}>
            <circle className="yf-trend-point-ring" cx={point.x} cy={point.y} r="5" fill="#fff" stroke={color.main} />
            <circle className="yf-trend-point" cx={point.x} cy={point.y} r="2.5" fill={color.main} />
            {config.showPointLabels ? (
              <text className="yf-trend-point-label" x={point.x} y={point.y - 10}>{this.formatNumber(point.value)}</text>
            ) : null}
          </g>
        ))}
      </g>
    );
  }

  renderXAxisLabels(points: TrendPoint[], chartHeight: number) {
    const step = Math.max(1, Math.ceil(points.length / 8));
    return (
      <g>
        {points.map((point, index) => {
          if (index % step !== 0 && index !== points.length - 1) {
            return null;
          }
          return (
            <text className="yf-trend-x-label" key={'x-' + point.key + index} x={point.x} y={chartHeight - 14}>
              {point.label}
            </text>
          );
        })}
      </g>
    );
  }

  buildLinePath(points: TrendPoint[]) {
    if (!points.length) {
      return '';
    }
    return points.map((point, index) => (index === 0 ? 'M ' : 'L ') + point.x + ' ' + point.y).join(' ');
  }

  buildAreaPath(points: TrendPoint[], baseline: number) {
    if (!points.length) {
      return '';
    }
    const line = this.buildLinePath(points);
    const last = points[points.length - 1];
    const first = points[0];
    return line + ' L ' + last.x + ' ' + baseline + ' L ' + first.x + ' ' + baseline + ' Z';
  }

  normalizeChartType(value: string) {
    const text = (value || '').toLowerCase();
    if (text === 'area' || text === 'arealine' || text === 'area-line') {
      return 'area';
    }
    if (text === 'column' || text === 'bar' || text === 'verticalbar' || text === 'vertical-bar') {
      return 'column';
    }
    return 'line';
  }

  normalizeGranularity(value: string) {
    const text = (value || '').toLowerCase();
    if (text === 'week' || text === 'weekly') {
      return 'week';
    }
    if (text === 'month' || text === 'monthly') {
      return 'month';
    }
    return 'day';
  }

  normalizeSortMode(value: string) {
    const text = (value || '').toLowerCase();
    if (text === 'reversechronological' || text === 'reverse-chronological' || text === 'newestfirst' || text === 'desc') {
      return 'reverseChronological';
    }
    return 'chronological';
  }

  normalizeDateRangeMode(value: string) {
    const text = (value || '').toLowerCase();
    if (text === 'last7days' || text === 'last-7-days' || text === '7') {
      return 'last7Days';
    }
    if (text === 'last30days' || text === 'last-30-days' || text === '30') {
      return 'last30Days';
    }
    if (text === 'last90days' || text === 'last-90-days' || text === '90') {
      return 'last90Days';
    }
    if (text === 'thismonth' || text === 'this-month') {
      return 'thisMonth';
    }
    if (text === 'last12months' || text === 'last-12-months' || text === '12months') {
      return 'last12Months';
    }
    return 'all';
  }

  normalizeColorMode(value: string) {
    const text = (value || '').toLowerCase();
    if (text === 'green' || text === 'amber' || text === 'red' || text === 'violet' || text === 'slate') {
      return text;
    }
    return 'yeeflow';
  }

  getPalette(colorMode: string) {
    if (colorMode === 'green') {
      return { main: '#12B76A', area: 'rgba(18,183,106,.14)' };
    }
    if (colorMode === 'amber') {
      return { main: '#F59E0B', area: 'rgba(245,158,11,.16)' };
    }
    if (colorMode === 'red') {
      return { main: '#E45665', area: 'rgba(228,86,101,.14)' };
    }
    if (colorMode === 'violet') {
      return { main: '#7A5AF8', area: 'rgba(122,90,248,.14)' };
    }
    if (colorMode === 'slate') {
      return { main: '#315A8C', area: 'rgba(49,90,140,.14)' };
    }
    return { main: '#146FF6', area: 'rgba(20,111,246,.14)' };
  }

  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;
  }

  cloneDate(value: Date) {
    return new Date(value.getFullYear(), value.getMonth(), value.getDate());
  }

  formatDateKey(date: Date, mode: string) {
    const year = date.getFullYear();
    const month = this.pad2(date.getMonth() + 1);
    const day = this.pad2(date.getDate());
    if (mode === 'month') {
      return year + '-' + month;
    }
    return year + '-' + month + '-' + day;
  }

  formatShortDate(date: Date) {
    return this.monthName(date.getMonth()) + ' ' + date.getDate();
  }

  formatMonthLabel(date: Date) {
    return this.monthName(date.getMonth()) + ' ' + date.getFullYear();
  }

  monthName(index: number) {
    return ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][index] || '';
  }

  pad2(value: number) {
    return value < 10 ? '0' + value : String(value);
  }

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

  getStyles() {
    return [
      '.yf-trend-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-trend-module *{box-sizing:border-box;}',
      '.yf-trend-header{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:16px;}',
      '.yf-trend-title{font-size:16px;line-height:22px;font-weight:700;color:#101828;}',
      '.yf-trend-subtitle{margin-top:3px;font-size:12px;line-height:18px;color:#667085;}',
      '.yf-trend-summary{flex:0 0 auto;display:flex;align-items:baseline;gap:5px;background:#f8fafc;border:1px solid #e5eaf3;border-radius:999px;padding:4px 10px;color:#475467;}',
      '.yf-trend-summary span{font-size:12px;line-height:18px;font-weight:700;color:#101828;font-variant-numeric:tabular-nums;}',
      '.yf-trend-summary em{font-size:12px;line-height:18px;font-style:normal;color:#667085;}',
      '.yf-trend-body{display:flex;flex-direction:column;gap:8px;min-width:0;}',
      '.yf-trend-chart-wrap{width:100%;min-height:220px;}',
      '.yf-trend-chart{display:block;width:100%;height:100%;min-height:220px;overflow:visible;}',
      '.yf-trend-grid{stroke:#edf1f7;stroke-width:1;}',
      '.yf-trend-y-label{fill:#98a2b3;font-size:11px;text-anchor:end;font-variant-numeric:tabular-nums;}',
      '.yf-trend-line{fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;}',
      '.yf-trend-area{stroke:none;}',
      '.yf-trend-point-ring{stroke-width:2;}',
      '.yf-trend-point-label{fill:#344054;font-size:11px;font-weight:650;text-anchor:middle;font-variant-numeric:tabular-nums;}',
      '.yf-trend-x-label{fill:#667085;font-size:11px;text-anchor:middle;}',
      '.yf-trend-column{opacity:.92;}',
      '.yf-trend-note{font-size:12px;line-height:18px;color:#667085;text-align:center;}',
      '.yf-trend-empty,.yf-trend-loading{min-height:200px;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-trend-error{color:#b42318;background:#fff7f6;border-color:#fecdca;}',
      '.yf-trend-loading{gap:10px;}',
      '.yf-trend-spinner{width:16px;height:16px;border-radius:999px;border:2px solid #d8deea;border-top-color:#146ff6;animation:yfTrendSpin 900ms linear infinite;}',
      '@keyframes yfTrendSpin{to{transform:rotate(360deg);}}',
      '@media(max-width:640px){.yf-trend-module{padding:16px;}.yf-trend-header{flex-direction:column;align-items:flex-start;}.yf-trend-body{min-height:0!important;}.yf-trend-chart-wrap{overflow-x:auto;}.yf-trend-chart{min-width:520px;}.yf-trend-summary{align-self:flex-start;}}',
    ].join('');
  }
}
