// UIWS CalendarView 정본 구조 이식(월/주/일 뷰 전환·기간 막대·+N 더보기).
// 정본: workspace/uiws/frontend/src/components/CalendarView.tsx + calendarView.css.
// kintex 이식: 클래스/토큰만 kx-cv-* + kintex 디자인 토큰으로 번역. 외부 캘린더 라이브러리 미사용.
// 데이터는 controlled `items`(부모가 range별 조회) + onRangeChange 로 기존 데이터 소스 유지.
// 코드값 훅(useCodeValues)·공용 Modal 의존 없이 자체 경량 구현(kx 토큰).
import { useEffect, useMemo, useState, type CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import { IconChevronLeft, IconChevronRight, IconClose } from '../../components/ui/icons';
import './calendarView.css';

export interface KxCalendarItem {
  /** 안정 키. 부모에서 문자열로 정규화. */
  id: string;
  /** 기간 시작일(yyyy-MM-dd) */
  startDate: string;
  /** 기간 종료일(yyyy-MM-dd, 포함). 미지정/시작일과 동일 → 단일일. */
  endDate?: string;
  title: string;
  /** 막대/칩 강조색(hex) — 유형별 색(work 일정 타입 등). 미지정 시 기본 primary. */
  color?: string;
  /** 소형 텍스트 뱃지(유형 라벨 등) */
  badge?: string;
  /** 진행상태 색점 코드(UPCOMING/ONGOING/DONE). */
  statusCd?: string;
}

export interface KxCalendarLegendItem {
  code: string;
  label: string;
  color?: string;
}

export type KxCalendarRange = { fromDate: string; toDate: string };
type View = 'month' | 'week' | 'day';

interface KxCalendarViewProps {
  /** 부모가 현재 range 로 조회해 넘기는 항목(controlled). */
  items: KxCalendarItem[];
  /** 뷰/기준일이 바뀌어 표시 범위가 달라질 때 통지(부모가 그 범위로 재조회). */
  onRangeChange?: (range: KxCalendarRange, view: View) => void;
  /** 빈 날짜(셀) 클릭 */
  onDateClick?: (date: string) => void;
  /** 항목 클릭 */
  onItemClick?: (item: KxCalendarItem) => void;
  /** 상태 색점 범례 */
  legend?: KxCalendarLegendItem[];
  /** 당일(yyyy-MM-dd). 미지정 시 실제 오늘. */
  today?: string;
  /** 초기 기준일. 미지정 시 today. */
  initialDate?: string;
  initialView?: View;
  /** 외부 선택일 하이라이트(work 어젠다 연동) */
  selectedDate?: string | null;
  loading?: boolean;
  /**
   * 공휴일 세트(yyyy-MM-dd → 공휴일명). 지정 시 해당 날짜 셀을 붉은색으로 표시하고
   * 날짜 숫자 옆에 공휴일명을 작은 라벨로 노출한다(일요일과 동일 톤 --color-error).
   * 미지정(undefined) 시 기존 동작과 완전 동일(파괴적 변경 없음).
   */
  holidays?: Map<string, string>;
}

/** 한 주(week-row) 안에서 차지할 수 있는 최대 레인 수(초과 시 "+N"). */
const MAX_LANES = 3;
/** 월 뷰 한 날짜 셀에 표시할 단일일 항목 최대 건수(초과 시 "+N"). */
const MAX_CELL_ITEMS = 4;

/* ===================== 날짜 헬퍼(utils/date 인라인 이식) ===================== */
function pad(n: number): string {
  return String(n).padStart(2, '0');
}
function fmt(d: Date): string {
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
function realTodayYmd(): string {
  return fmt(new Date());
}
function parseYmd(ymd: string): Date {
  const [y, m, d] = ymd.split('-').map(Number);
  return new Date(y, (m || 1) - 1, d || 1);
}
function ymdPart(s: string): string {
  return (s || '').slice(0, 10);
}
function diffDays(a: string, b: string): number {
  return Math.round((parseYmd(b).getTime() - parseYmd(a).getTime()) / 86_400_000);
}
function shiftDays(ymd: string, n: number): string {
  const d = parseYmd(ymd);
  d.setDate(d.getDate() + n);
  return fmt(d);
}
function shiftMonth(ymd: string, n: number): string {
  const d = parseYmd(ymd);
  return fmt(new Date(d.getFullYear(), d.getMonth() + n, 1));
}
function dayOf(ymd: string): number {
  return parseYmd(ymd).getDate();
}
function startOfWeek(ymd: string): string {
  const d = parseYmd(ymd);
  d.setDate(d.getDate() - d.getDay());
  return fmt(d);
}
function weekDates(ymd: string): string[] {
  const s = startOfWeek(ymd);
  return Array.from({ length: 7 }, (_, i) => shiftDays(s, i));
}
function monthGrid(ymd: string): { date: string; inMonth: boolean }[] {
  const base = parseYmd(ymd);
  const first = new Date(base.getFullYear(), base.getMonth(), 1);
  const start = new Date(first);
  start.setDate(1 - first.getDay());
  const month = base.getMonth();
  return Array.from({ length: 42 }, (_, i) => {
    const c = new Date(start);
    c.setDate(start.getDate() + i);
    return { date: fmt(c), inMonth: c.getMonth() === month };
  });
}

/* ===================== 항목 파생 ===================== */
function itemSpan(it: KxCalendarItem): { start: string; end: string } {
  const start = ymdPart(it.startDate);
  let end = ymdPart(it.endDate ?? it.startDate);
  if (!end || diffDays(start, end) < 0) end = start;
  return { start, end };
}
function isMultiDay(it: KxCalendarItem): boolean {
  const { start, end } = itemSpan(it);
  return !!start && diffDays(start, end) >= 1;
}
/** 셀 내 정렬(신규 우선) — id 사전식 desc. */
function newestFirst(a: KxCalendarItem, b: KxCalendarItem): number {
  return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
}

function rangeOf(view: View, baseDate: string): KxCalendarRange {
  if (view === 'month') {
    const grid = monthGrid(baseDate);
    return { fromDate: grid[0].date, toDate: grid[grid.length - 1].date };
  }
  if (view === 'week') {
    const w = weekDates(baseDate);
    return { fromDate: w[0], toDate: w[6] };
  }
  return { fromDate: baseDate, toDate: baseDate };
}

/* ===================== 기간 막대 레인 배치 ===================== */
interface BarSegment {
  item: KxCalendarItem;
  startCol: number;
  span: number;
  lane: number;
  continuesLeft: boolean;
  continuesRight: boolean;
}

function layoutWeekBars(
  cols: string[],
  multiItems: KxCalendarItem[],
  laneHint: Map<string, number>,
): { segments: BarSegment[]; usedLanes: number } {
  const weekStart = cols[0];
  const weekEnd = cols[6];

  type Pending = Omit<BarSegment, 'lane'>;
  const pending: Pending[] = [];
  for (const item of multiItems) {
    const { start, end } = itemSpan(item);
    if (diffDays(end, weekStart) > 0 || diffDays(start, weekEnd) > 0) continue;
    const clipStart = diffDays(start, weekStart) > 0 ? weekStart : start;
    const clipEnd = diffDays(end, weekEnd) < 0 ? end : weekEnd;
    const startCol = diffDays(weekStart, clipStart);
    const span = diffDays(clipStart, clipEnd) + 1;
    pending.push({
      item,
      startCol,
      span,
      continuesLeft: diffDays(start, weekStart) > 0,
      continuesRight: diffDays(end, weekEnd) < 0,
    });
  }
  pending.sort((a, b) => a.startCol - b.startCol || b.span - a.span);

  const laneMasks: number[] = [];
  const maskOf = (startCol: number, span: number) => {
    let m = 0;
    for (let c = startCol; c < startCol + span; c++) m |= 1 << c;
    return m;
  };
  const fits = (lane: number, mask: number) => ((laneMasks[lane] ?? 0) & mask) === 0;

  const segments: BarSegment[] = [];
  let usedLanes = 0;
  for (const p of pending) {
    const mask = maskOf(p.startCol, p.span);
    let lane = -1;
    const hinted = laneHint.get(p.item.id);
    if (hinted != null && fits(hinted, mask)) {
      lane = hinted;
    } else {
      for (let l = 0; ; l++) {
        if (fits(l, mask)) {
          lane = l;
          break;
        }
      }
    }
    laneMasks[lane] = (laneMasks[lane] ?? 0) | mask;
    laneHint.set(p.item.id, lane);
    usedLanes = Math.max(usedLanes, lane + 1);
    segments.push({ ...p, lane });
  }
  return { segments, usedLanes };
}

/* ===================== 본체 ===================== */
export default function KxCalendarView({
  items,
  onRangeChange,
  onDateClick,
  onItemClick,
  legend,
  today,
  initialDate,
  initialView = 'month',
  selectedDate,
  loading,
  holidays,
}: KxCalendarViewProps) {
  const { t } = useTranslation();
  const todayYmd = today ?? realTodayYmd();
  const [view, setView] = useState<View>(initialView);
  const [baseDate, setBaseDate] = useState(initialDate ?? todayYmd);
  const [moreDate, setMoreDate] = useState<string | null>(null);

  // 표시 범위 변경 통지(부모 재조회).
  useEffect(() => {
    onRangeChange?.(rangeOf(view, baseDate), view);
    // onRangeChange 는 의도적으로 의존성에서 제외(부모가 매 렌더 새 함수 전달 시 루프 방지).
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [view, baseDate]);

  const singleByDate = useMemo(() => {
    const m = new Map<string, KxCalendarItem[]>();
    items.forEach((it) => {
      if (isMultiDay(it)) return;
      const key = itemSpan(it).start;
      if (!key) return;
      const arr = m.get(key) ?? [];
      arr.push(it);
      m.set(key, arr);
    });
    return m;
  }, [items]);

  const multiItems = useMemo(() => items.filter(isMultiDay), [items]);

  const eventsOnDate = (date: string): KxCalendarItem[] => {
    const spanning = multiItems.filter((it) => {
      const { start, end } = itemSpan(it);
      return diffDays(start, date) >= 0 && diffDays(date, end) >= 0;
    });
    const singles = [...(singleByDate.get(date) ?? [])].sort(newestFirst);
    return [...spanning, ...singles];
  };

  const move = (delta: number) => {
    if (view === 'month') setBaseDate((b) => shiftMonth(b, delta));
    else if (view === 'week') setBaseDate((b) => shiftDays(b, delta * 7));
    else setBaseDate((b) => shiftDays(b, delta));
  };

  const title = useMemo(() => {
    if (view === 'month') {
      const d = parseYmd(baseDate);
      return t('calendar.yearMonth', { y: d.getFullYear(), m: d.getMonth() + 1 });
    }
    if (view === 'week') {
      const w = weekDates(baseDate);
      return `${w[0]} ~ ${w[6]}`;
    }
    return baseDate;
  }, [view, baseDate, t]);

  const renderItem = (it: KxCalendarItem) => (
    <button
      key={it.id}
      type="button"
      className={`kx-cv-event${it.statusCd === 'DONE' ? ' is-done' : ''}`}
      style={it.color ? ({ ['--kx-bar-color']: it.color } as CSSProperties) : undefined}
      title={it.title}
      onClick={(e) => {
        e.stopPropagation();
        onItemClick?.(it);
      }}
    >
      {it.statusCd && <span className={`kx-cv-dot kx-cv-imp-${it.statusCd}`} aria-hidden="true" />}
      {it.badge && <span className="kx-cv-badge">{it.badge}</span>}
      <span className="kx-cv-event-title">{it.title}</span>
    </button>
  );

  const renderBar = (seg: BarSegment) => {
    const it = seg.item;
    const cls = [
      'kx-cv-bar',
      seg.continuesLeft ? 'is-cont-left' : '',
      seg.continuesRight ? 'is-cont-right' : '',
      it.statusCd === 'DONE' ? 'is-done' : '',
    ]
      .filter(Boolean)
      .join(' ');
    return (
      <button
        key={`${it.id}-${seg.startCol}`}
        type="button"
        className={cls}
        style={{
          gridColumn: `${seg.startCol + 1} / span ${seg.span}`,
          gridRow: seg.lane + 1,
          ...(it.color ? ({ ['--kx-bar-color']: it.color } as CSSProperties) : {}),
        }}
        title={it.title}
        onClick={(e) => {
          e.stopPropagation();
          onItemClick?.(it);
        }}
      >
        {!seg.continuesLeft && it.badge && <span className="kx-cv-bar-badge">{it.badge}</span>}
        <span className="kx-cv-bar-title">{it.title}</span>
      </button>
    );
  };

  return (
    <div className="kx-cv-root">
      <div className="kx-cv-toolbar">
        <button type="button" className="kx-cv-nav" onClick={() => move(-1)}>
          <IconChevronLeft size={14} /> {t('calendar.prev')}
        </button>
        <strong className="kx-cv-title tnum" aria-live="polite">
          {title}
        </strong>
        <button type="button" className="kx-cv-nav" onClick={() => move(1)}>
          {t('calendar.next')} <IconChevronRight size={14} />
        </button>
        <button type="button" className="kx-cv-nav" onClick={() => setBaseDate(todayYmd)}>
          {t('calendar.today')}
        </button>
        <span className="kx-cv-switch" role="group" aria-label={t('calendar.viewSwitch')}>
          {(['month', 'week', 'day'] as View[]).map((v) => (
            <button
              key={v}
              type="button"
              className={view === v ? 'is-active' : ''}
              aria-pressed={view === v}
              onClick={() => setView(v)}
            >
              {t(`calendar.view.${v}`)}
            </button>
          ))}
        </span>
        {loading && <span className="kx-cv-muted">{t('calendar.loading')}</span>}
      </div>

      {legend && legend.length > 0 && (
        <div className="kx-cv-legend" aria-label={t('calendar.legend')}>
          {legend.map((c) => (
            <span key={c.code} className="kx-cv-legend-item">
              <span
                className={`kx-cv-dot${c.color ? '' : ` kx-cv-imp-${c.code}`}`}
                style={c.color ? { background: c.color } : undefined}
                aria-hidden="true"
              />
              {c.label}
            </span>
          ))}
        </div>
      )}

      {view === 'month' && (
        <MonthView
          baseDate={baseDate}
          today={todayYmd}
          selectedDate={selectedDate ?? null}
          singleByDate={singleByDate}
          multiItems={multiItems}
          onDateClick={onDateClick}
          onMore={setMoreDate}
          renderItem={renderItem}
          renderBar={renderBar}
          holidays={holidays}
        />
      )}

      {view === 'week' && (
        <WeekView
          baseDate={baseDate}
          today={todayYmd}
          selectedDate={selectedDate ?? null}
          singleByDate={singleByDate}
          multiItems={multiItems}
          onDateClick={onDateClick}
          onMore={setMoreDate}
          renderItem={renderItem}
          renderBar={renderBar}
          holidays={holidays}
        />
      )}

      {view === 'day' && (
        <div
          className={`kx-cv-day${holidays?.has(baseDate) ? ' is-holiday' : ''}`}
          onClick={() => onDateClick?.(baseDate)}
          role={onDateClick ? 'button' : undefined}
        >
          <div className="kx-cv-day-head">
            {baseDate}
            {holidays?.get(baseDate) && (
              <span className="kx-cv-holiday-name">{holidays.get(baseDate)}</span>
            )}
          </div>
          {(() => {
            const all = eventsOnDate(baseDate);
            if (all.length === 0) {
              return <p className="kx-cv-muted">{t('calendar.dayEmpty')}</p>;
            }
            return <div className="kx-cv-day-list">{all.map(renderItem)}</div>;
          })()}
        </div>
      )}

      {moreDate && (
        <div className="kx-cv-modal-backdrop" onClick={() => setMoreDate(null)}>
          <div
            className="kx-cv-modal"
            role="dialog"
            aria-modal="true"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="kx-cv-modal-head">
              <strong>{t('calendar.dayAll', { date: moreDate })}</strong>
              <button
                type="button"
                className="kx-cv-modal-close"
                aria-label={t('calendar.close')}
                onClick={() => setMoreDate(null)}
              >
                <IconClose size={16} />
              </button>
            </div>
            {(() => {
              const dayItems = eventsOnDate(moreDate);
              if (dayItems.length === 0) {
                return <p className="kx-cv-muted">{t('calendar.dayEmpty')}</p>;
              }
              return (
                <ul className="kx-cv-more-list">
                  {dayItems.map((it) => {
                    const { start, end } = itemSpan(it);
                    return (
                      <li key={it.id}>
                        <button
                          type="button"
                          className="kx-cv-more-item"
                          onClick={() => {
                            setMoreDate(null);
                            onItemClick?.(it);
                          }}
                        >
                          {it.statusCd && (
                            <span
                              className={`kx-cv-dot kx-cv-imp-${it.statusCd}`}
                              aria-hidden="true"
                            />
                          )}
                          {it.badge && <span className="kx-cv-badge">{it.badge}</span>}
                          <span className="kx-cv-more-item-title">{it.title}</span>
                          <span className="kx-cv-more-item-range">
                            {isMultiDay(it) ? `${start} ~ ${end}` : start}
                          </span>
                        </button>
                      </li>
                    );
                  })}
                </ul>
              );
            })()}
          </div>
        </div>
      )}
    </div>
  );
}

/* ===================== 월 뷰 ===================== */
interface WeekViewShared {
  today: string;
  selectedDate: string | null;
  singleByDate: Map<string, KxCalendarItem[]>;
  multiItems: KxCalendarItem[];
  onDateClick?: (date: string) => void;
  onMore: (date: string) => void;
  renderItem: (it: KxCalendarItem) => JSX.Element;
  renderBar: (seg: BarSegment) => JSX.Element;
  /** 공휴일 세트(ymd → 명칭). 미지정 시 표시 없음. */
  holidays?: Map<string, string>;
}

function MonthView({ baseDate, ...rest }: { baseDate: string } & WeekViewShared) {
  const { t } = useTranslation();
  const grid = monthGrid(baseDate);
  const weeks: { date: string; inMonth: boolean }[][] = [];
  for (let w = 0; w < grid.length; w += 7) weeks.push(grid.slice(w, w + 7));
  const laneHint = new Map<string, number>();

  return (
    <div className="kx-cv-grid">
      <div className="kx-cv-dow-row">
        {[0, 1, 2, 3, 4, 5, 6].map((i) => (
          <div key={i} className={`kx-cv-dow${i === 0 ? ' is-sun' : i === 6 ? ' is-sat' : ''}`}>
            {t(`common.weekday.${i}`)}
          </div>
        ))}
      </div>
      {weeks.map((week) => (
        <MonthWeekRow key={week[0].date} week={week} laneHint={laneHint} {...rest} />
      ))}
    </div>
  );
}

function MonthWeekRow({
  week,
  laneHint,
  today,
  selectedDate,
  singleByDate,
  multiItems,
  onDateClick,
  onMore,
  renderItem,
  renderBar,
  holidays,
}: {
  week: { date: string; inMonth: boolean }[];
  laneHint: Map<string, number>;
} & WeekViewShared) {
  const cols = week.map((c) => c.date);
  const { segments, usedLanes } = layoutWeekBars(cols, multiItems, laneHint);
  const lanes = Math.min(usedLanes, MAX_LANES);
  const barAreaStyle = { ['--kx-cv-bar-lanes']: String(lanes) } as CSSProperties;
  const hiddenBars = segments.filter((s) => s.lane >= MAX_LANES);

  return (
    <div className="kx-cv-week-row" style={barAreaStyle}>
      <div className="kx-cv-week-cells">
        {week.map(({ date, inMonth }, dow) => {
          const singles = singleByDate.get(date) ?? [];
          const holidayName = holidays?.get(date);
          const cls = [
            'kx-cv-cell',
            inMonth ? '' : 'is-out',
            date === today ? 'is-today' : '',
            date === selectedDate ? 'is-selected' : '',
            dow === 0 ? 'is-sun' : dow === 6 ? 'is-sat' : '',
            holidayName ? 'is-holiday' : '',
          ]
            .filter(Boolean)
            .join(' ');
          const hiddenHere = hiddenBars.filter((s) => s.startCol === dow).length;
          const ordered = [...singles].sort(newestFirst);
          const shown = ordered.slice(0, MAX_CELL_ITEMS);
          const moreCount = ordered.length - shown.length + hiddenHere;
          return (
            <div key={date} className={cls} onClick={() => onDateClick?.(date)}>
              <span className="kx-cv-daynum">
                {dayOf(date)}
                {holidayName && (
                  <em className="kx-cv-holiday-name" title={holidayName}>
                    {holidayName}
                  </em>
                )}
              </span>
              <div className="kx-cv-cell-singles">
                {shown.map(renderItem)}
                {moreCount > 0 && (
                  <button
                    type="button"
                    className="kx-cv-more"
                    onClick={(e) => {
                      e.stopPropagation();
                      onMore(date);
                    }}
                  >
                    +{moreCount}
                  </button>
                )}
              </div>
            </div>
          );
        })}
      </div>
      {lanes > 0 && (
        <div className="kx-cv-bars">{segments.filter((s) => s.lane < MAX_LANES).map(renderBar)}</div>
      )}
    </div>
  );
}

/* ===================== 주 뷰 ===================== */
function WeekView({
  baseDate,
  today,
  selectedDate,
  singleByDate,
  multiItems,
  onDateClick,
  onMore,
  renderItem,
  renderBar,
  holidays,
}: { baseDate: string } & WeekViewShared) {
  const { t } = useTranslation();
  const cols = weekDates(baseDate);
  const laneHint = new Map<string, number>();
  const { segments, usedLanes } = layoutWeekBars(cols, multiItems, laneHint);
  const lanes = Math.min(usedLanes, MAX_LANES);
  const hiddenBars = segments.filter((s) => s.lane >= MAX_LANES);

  return (
    <div className="kx-cv-week-view">
      <div className="kx-cv-week-grid">
        {cols.map((date, i) => {
          const holidayName = holidays?.get(date);
          const dowCls = i === 0 ? ' is-sun' : i === 6 ? ' is-sat' : '';
          return (
            <div key={`h-${date}`} className={`kx-cv-dow${dowCls}${holidayName ? ' is-holiday' : ''}`}>
              {t(`common.weekday.${i}`)} {dayOf(date)}
              {holidayName && <em className="kx-cv-holiday-name" title={holidayName}>{holidayName}</em>}
            </div>
          );
        })}
      </div>
      {lanes > 0 && (
        <div
          className="kx-cv-week-bars"
          style={{ ['--kx-cv-bar-lanes']: String(lanes) } as CSSProperties}
        >
          {segments.filter((s) => s.lane < MAX_LANES).map(renderBar)}
        </div>
      )}
      <div className="kx-cv-week-grid kx-cv-week-body">
        {cols.map((date, dow) => {
          const singles = singleByDate.get(date) ?? [];
          const hiddenHere = hiddenBars.filter((s) => s.startCol === dow).length;
          return (
            <div
              key={date}
              className={`kx-cv-week-cell${date === today ? ' is-today' : ''}${date === selectedDate ? ' is-selected' : ''}`}
              onClick={() => onDateClick?.(date)}
            >
              {singles.map(renderItem)}
              {hiddenHere > 0 && (
                <button
                  type="button"
                  className="kx-cv-more"
                  onClick={(e) => {
                    e.stopPropagation();
                    onMore(date);
                  }}
                >
                  +{hiddenHere}
                </button>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}
