import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { cssValue, escapeAttr } from '../../utils/escape';

interface StarRatingProps {
  rating?: number;
  maxStars?: number;
  size?: string;
  filledColor?: string;
  emptyColor?: string;
  style?: CSSProperties;
  animation?: string;
  animationDelay?: string;
  hideOnDesktop?: boolean;
  hideOnTablet?: boolean;
  hideOnMobile?: boolean;
}

export const StarRating: UserComponent<StarRatingProps> = ({
  rating = 4.5,
  maxStars = 5,
  size = '24px',
  filledColor = '#f59e0b',
  emptyColor = '#d1d5db',
  style = {},
}) => {
  const {
    connectors: { connect, drag },
    selected,
  } = useNode((node) => ({
    selected: node.events.selected,
  }));

  const stars: React.ReactNode[] = [];
  for (let i = 1; i <= maxStars; i++) {
    if (i <= Math.floor(rating)) {
      // Full star
      stars.push(
        <i
          key={i}
          className="fa fa-star"
          style={{ color: filledColor, fontSize: size }}
        />
      );
    } else if (i === Math.ceil(rating) && rating % 1 !== 0) {
      // Half star
      stars.push(
        <span key={i} style={{ position: 'relative', display: 'inline-block', fontSize: size }}>
          <i className="fa fa-star" style={{ color: emptyColor }} />
          <span style={{ position: 'absolute', left: 0, top: 0, overflow: 'hidden', width: '50%' }}>
            <i className="fa fa-star" style={{ color: filledColor }} />
          </span>
        </span>
      );
    } else {
      // Empty star
      stars.push(
        <i
          key={i}
          className="fa fa-star"
          style={{ color: emptyColor, fontSize: size }}
        />
      );
    }
  }

  return (
    <span
      ref={(ref: HTMLSpanElement | null): void => { if (ref) connect(drag(ref)); }}
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        gap: '2px',
        outline: selected ? '2px solid #3b82f6' : 'none',
        ...style,
      }}
    >
      {stars}
    </span>
  );
};

/* ---------- Craft config ---------- */

StarRating.craft = {
  displayName: 'Star Rating',
  props: {
    rating: 4.5,
    maxStars: 5,
    size: '24px',
    filledColor: '#f59e0b',
    emptyColor: '#d1d5db',
    style: {},
    animation: '',
    animationDelay: '',
    hideOnDesktop: false,
    hideOnTablet: false,
    hideOnMobile: false,
  },
  rules: {
    canDrag: () => true,
    canMoveIn: () => false,
    canMoveOut: () => true,
  },
};

/* ---------- HTML export ---------- */

(StarRating as any).toHtml = (props: StarRatingProps, _childrenHtml: string) => {
  // `rating`/`maxStars` are declared `number` in TS but arrive unchecked at
  // runtime (AI update_props only validates node_id; deserialized saved
  // state is untyped JSON) -- a string like `5" onmouseover="alert(1)`
  // breaks out of the aria-label attribute below, and an uncoerced/unclamped
  // maxStars can also blow up the star-glyph loop (NaN, absurd loop count,
  // or -- observed -- a RangeError from string concatenation overflow with
  // e.g. maxStars=1e9). Coerce to numbers with sane fallbacks/clamps first.
  const ratingRaw = Number(props.rating);
  const rating = Number.isFinite(ratingRaw) ? ratingRaw : 4.5;
  const maxStarsRaw = Number(props.maxStars);
  const maxStars = Number.isFinite(maxStarsRaw)
    ? Math.min(Math.max(Math.trunc(maxStarsRaw), 0), 50)
    : 5;
  // Sanitized -- raw string-interpolation sinks in the star glyphs below.
  const size = cssValue(props.size) || '24px';
  const filledColor = cssValue(props.filledColor) || '#f59e0b';
  const emptyColor = cssValue(props.emptyColor) || '#d1d5db';
  const wrapperStyle = cssPropsToString({
    display: 'inline-flex',
    alignItems: 'center',
    gap: '2px',
    ...props.style,
  });

  let starsHtml = '';
  for (let i = 1; i <= maxStars; i++) {
    if (i <= Math.floor(rating)) {
      starsHtml += `<i class="fa fa-star" style="color:${filledColor};font-size:${size}" aria-hidden="true"></i>`;
    } else if (i === Math.ceil(rating) && rating % 1 !== 0) {
      starsHtml += `<span style="position:relative;display:inline-block;font-size:${size}" aria-hidden="true"><i class="fa fa-star" style="color:${emptyColor}"></i><span style="position:absolute;left:0;top:0;overflow:hidden;width:50%"><i class="fa fa-star" style="color:${filledColor}"></i></span></span>`;
    } else {
      starsHtml += `<i class="fa fa-star" style="color:${emptyColor};font-size:${size}" aria-hidden="true"></i>`;
    }
  }

  // The star glyphs convey nothing to assistive tech on their own -- wrap
  // in role="img" with a textual equivalent, and hide the decorative glyphs
  // themselves (aria-hidden above) so AT doesn't announce each icon.
  // Belt-and-suspenders: rating/maxStars are already coerced to numbers
  // above, but the assembled label is still run through escapeAttr() in
  // case a decimal/negative/Infinity edge case produces odd (though no
  // longer dangerous) text.
  const ariaLabel = escapeAttr(`Rating: ${rating} out of ${maxStars}`);
  return {
    html: `<span role="img" aria-label="${ariaLabel}"${wrapperStyle ? ` style="${wrapperStyle}"` : ''}>${starsHtml}</span>`,
  };
};
