import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { AvailabilityGrid } from '../availability/AvailabilityGrid';
import { MeetingProposalCard } from '../availability/MeetingProposalCard';
import { useConnection } from '../../context/ConnectionContext';
import { useServerData } from '../../context/ServerDataContext';
import {
  detectUserTimezone,
  formatHoverTime,
  formatMeetingRange,
  formatWeekHeader,
  getTimezoneShiftHours,
  getWeekNumber,
  getWeekStartDate,
  normalizeLinearSelection,
  shiftMeetingRange,
  shiftSlot,
  slotKey,
} from '../../data/availabilityUtils';
import { nearestTimezone } from '../../data/timezones';
import { TimezoneSelect } from '../TimezoneSelect';

const ASPECT_WIDE_MIN = 1.22;
const ASPECT_STACKED_MIN = 3;

export function AvailabilityPanel() {
  const containerRef = useRef(null);
  const teamSectionRef = useRef(null);
  const { account } = useConnection();
  const {
    users,
    availability,
    meetings,
    setMyAvailability,
    saveMeeting,
    removeMeeting,
    availabilityLoaded,
  } = useServerData();

  const [layout, setLayout] = useState('wide');
  const [activeWeek, setActiveWeek] = useState(() => getWeekNumber(new Date()));
  const nativeTimezone = useMemo(detectUserTimezone, []);
  // Default the view to the account's profile timezone; fall back to the
  // system zone snapped to the canonical list.
  const [timezone, setTimezone] = useState(
    () => account?.timezone ?? nearestTimezone(detectUserTimezone())
  );
  const [hoveredSlot, setHoveredSlot] = useState(null);
  const [selection, setSelection] = useState(null);
  const [proposal, setProposal] = useState(null);
  const [editingMeetingId, setEditingMeetingId] = useState(null);

  const weekHeader = useMemo(() => formatWeekHeader(activeWeek), [activeWeek]);

  // Positive shift = the selected timezone is ahead of the browser's; both
  // charts render as if every slot happened that many hours later/earlier.
  const shiftHours = useMemo(
    () => getTimezoneShiftHours(nativeTimezone, timezone, getWeekStartDate(activeWeek)),
    [nativeTimezone, timezone, activeWeek]
  );

  const activeMembers = useMemo(() => users.filter((u) => !u.removed), [users]);

  const availabilitySets = useMemo(() => {
    const map = {};
    for (const [username, slots] of Object.entries(availability)) {
      map[username.toLowerCase()] = new Set(slots);
    }
    return map;
  }, [availability]);

  const myAvailability = useMemo(
    () => availabilitySets[account?.username.toLowerCase()] ?? new Set(),
    [availabilitySets, account]
  );

  const overlapCount = useCallback(
    (dayIndex, hour) => {
      const key = slotKey(dayIndex, hour);
      return activeMembers.filter((u) =>
        availabilitySets[u.username.toLowerCase()]?.has(key)
      ).length;
    },
    [activeMembers, availabilitySets]
  );

  const weekMeetings = useMemo(
    () =>
      meetings
        .filter((m) => m.weekNumber === activeWeek)
        .sort(
          (a, b) =>
            a.startDay - b.startDay ||
            a.startHour - b.startHour ||
            a.endDay - b.endDay ||
            a.endHour - b.endHour
        ),
    [meetings, activeWeek]
  );

  const hoveredNames = useMemo(() => {
    if (!hoveredSlot) return [];
    // hoveredSlot is in display coordinates; the availability data is native.
    const native = shiftSlot(hoveredSlot.dayIndex, hoveredSlot.hour, -shiftHours);
    const key = slotKey(native.dayIndex, native.hour);
    return activeMembers
      .filter((u) => availabilitySets[u.username.toLowerCase()]?.has(key))
      .map((u) => u.displayName);
  }, [hoveredSlot, activeMembers, availabilitySets, shiftHours]);

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return undefined;

    const observer = new ResizeObserver(([entry]) => {
      const { width, height } = entry.contentRect;
      if (!width || !height) return;
      const aspect = width / height;
      if (aspect >= ASPECT_WIDE_MIN) {
        setLayout('wide');
      } else if (height / width >= ASPECT_STACKED_MIN) {
        setLayout('stacked');
      } else {
        setLayout('medium');
      }
    });

    observer.observe(el);
    return () => observer.disconnect();
  }, []);

  const handleApplyCells = useCallback(
    (cells, enabled) => {
      const next = new Set(myAvailability);
      for (const { dayIndex, hour } of cells) {
        const key = slotKey(dayIndex, hour);
        if (enabled) next.add(key);
        else next.delete(key);
      }
      // Optimistic in ServerDataContext; persisted via PUT /api/availability/me.
      setMyAvailability(next);
    },
    [myAvailability, setMyAvailability]
  );

  const openProposal = useCallback(
    (start, end, meeting = null) => {
      const { start: s, end: e } = normalizeLinearSelection(start, end);
      // `selection` stays native — the grid and the popup anchor both key off it.
      setSelection({ start: s, end: e });
      // The popup's own time fields show the viewer's selected timezone, matching
      // where the highlighted range visually sits in the shifted grid.
      const display = shiftMeetingRange(
        { startDay: s.dayIndex, startHour: s.hour, endDay: e.dayIndex, endHour: e.hour + 1 },
        shiftHours
      );
      setProposal({
        startDay: display.startDay,
        startHour: display.startHour,
        endDay: display.endDay,
        endHour: display.endHour,
        subject: meeting?.subject ?? '',
      });
      setEditingMeetingId(meeting?.id ?? null);
    },
    [shiftHours]
  );

  const handleSelectionComplete = useCallback(
    ({ start, end }) => {
      openProposal(start, end);
    },
    [openProposal]
  );

  const handleMeetingClick = useCallback(
    (meeting) => {
      openProposal(
        { dayIndex: meeting.startDay, hour: meeting.startHour },
        { dayIndex: meeting.endDay, hour: meeting.endHour - 1 },
        meeting
      );
    },
    [openProposal]
  );

  const closeProposal = useCallback(() => {
    setProposal(null);
    setSelection(null);
    setEditingMeetingId(null);
  }, []);

  const handleSelectionStart = useCallback(() => {
    closeProposal();
  }, [closeProposal]);

  const handleSaveProposal = useCallback(() => {
    if (!proposal) return;
    // proposal's fields are in the viewer's display timezone; store native.
    const native = shiftMeetingRange(
      {
        startDay: proposal.startDay,
        startHour: proposal.startHour,
        endDay: proposal.endDay,
        endHour: proposal.endHour,
      },
      -shiftHours
    );
    // The server records the proposer from the session.
    saveMeeting(
      {
        weekNumber: activeWeek,
        startDay: native.startDay,
        startHour: native.startHour,
        endDay: native.endDay,
        endHour: native.endHour,
        subject: proposal.subject.trim() || 'Untitled meeting',
      },
      editingMeetingId
    );
    closeProposal();
  }, [proposal, editingMeetingId, activeWeek, saveMeeting, closeProposal, shiftHours]);

  const handleDeleteProposal = useCallback(() => {
    if (editingMeetingId) removeMeeting(editingMeetingId);
    closeProposal();
  }, [editingMeetingId, removeMeeting, closeProposal]);

  const [proposalPos, setProposalPos] = useState(null);
  const proposalOpen = Boolean(proposal);

  // Escape or a click outside the card discards it (no confirmation, whether
  // it's a fresh selection or an in-progress edit). MeetingProposalCard stops
  // mousedown propagation on itself, so this only ever fires for outside clicks.
  useEffect(() => {
    if (!proposalOpen) return undefined;
    const onKeyDown = (e) => {
      if (e.key === 'Escape') closeProposal();
    };
    const onMouseDown = () => closeProposal();
    document.addEventListener('keydown', onKeyDown);
    document.addEventListener('mousedown', onMouseDown);
    return () => {
      document.removeEventListener('keydown', onKeyDown);
      document.removeEventListener('mousedown', onMouseDown);
    };
  }, [proposalOpen, closeProposal]);

  // Anchor the popup just below the last highlighted cell — the cell the
  // mouse released on for a fresh drag, or the meeting's last cell when
  // reopened via the grid or the upcoming-meetings list.
  useLayoutEffect(() => {
    const container = teamSectionRef.current;
    if (!proposal || !selection || !container) {
      setProposalPos(null);
      return;
    }
    const display = shiftSlot(selection.end.dayIndex, selection.end.hour, shiftHours);
    const cellEl = container.querySelector(`[data-slot="${slotKey(display.dayIndex, display.hour)}"]`);
    if (!cellEl) {
      setProposalPos(null);
      return;
    }
    const measure = () => {
      const cellRect = cellEl.getBoundingClientRect();
      const containerRect = container.getBoundingClientRect();
      const cardWidth = 280;
      const maxLeft = Math.max(0, container.clientWidth - cardWidth - 8);
      const belowCell = cellRect.bottom - containerRect.top + 6;
      // If opening below the cell would run the card past the bottom edge,
      // shift it up just enough to stay fully in view instead of cropping.
      const cardEl = container.querySelector('.proposal-card');
      const cardHeight = cardEl?.getBoundingClientRect().height ?? 0;
      const maxTop = Math.max(6, container.clientHeight - cardHeight - 8);
      setProposalPos({
        top: Math.min(belowCell, maxTop),
        left: Math.min(cellRect.left - containerRect.left, maxLeft),
      });
    };
    measure();
    // The grid can still be settling its own layout right as a drag ends
    // (e.g. the sidebar's hover text changing height), and the card's own
    // size only stabilizes once its width style takes effect — re-measure
    // whenever either changes rather than guessing a fixed delay.
    const resizeObserver = new ResizeObserver(measure);
    resizeObserver.observe(container);
    const cardEl = container.querySelector('.proposal-card');
    if (cardEl) resizeObserver.observe(cardEl);
    return () => resizeObserver.disconnect();
  }, [proposal, selection, shiftHours]);

  const proposalStyle = useMemo(() => {
    if (!proposal || !proposalPos) return undefined;
    return {
      position: 'absolute',
      top: proposalPos.top,
      left: proposalPos.left,
      zIndex: 100,
      width: 'min(280px, 90%)',
    };
  }, [proposal, proposalPos]);

  const hoverHeading = hoveredSlot
    ? `Available at ${formatHoverTime(hoveredSlot.hour)}`
    : 'Available at —';

  return (
    <div ref={containerRef} className={`availability-container availability-container--${layout}`}>
      {!availabilityLoaded && <div className="panel-loading-hint">Loading availability…</div>}
      <div className="availability-toolbar">
        <div className="availability-week-nav">
          <button
            type="button"
            className="availability-week-btn"
            aria-label="Previous week"
            onClick={() => setActiveWeek((w) => Math.max(1, w - 1))}
          >
            <img src="assets/arrow_left.svg" alt="" />
          </button>
          <span className="availability-week-label">
            <strong>{weekHeader.label}</strong> {weekHeader.range}
          </span>
          <button
            type="button"
            className="availability-week-btn"
            aria-label="Next week"
            onClick={() => setActiveWeek((w) => w + 1)}
          >
            <img src="assets/arrow_right.svg" alt="" />
          </button>
        </div>

        <div className="availability-toolbar-right">
          <div className="availability-legend" aria-hidden="true">
            <span className="availability-legend-label">Less</span>
            <span className="availability-legend-swatch availability-legend-swatch--0" />
            <span className="availability-legend-swatch availability-legend-swatch--1" />
            <span className="availability-legend-swatch availability-legend-swatch--2" />
            <span className="availability-legend-swatch availability-legend-swatch--3" />
            <span className="availability-legend-swatch availability-legend-swatch--4" />
            <span className="availability-legend-label">More</span>
          </div>
          <TimezoneSelect
            className="availability-timezone"
            value={timezone}
            onChange={setTimezone}
            aria-label="Timezone"
          />
        </div>
      </div>

      <div className="availability-body">
        <div className="availability-charts">
          <AvailabilityGrid
            mode="personal"
            weekNumber={activeWeek}
            availableSlots={myAvailability}
            onApplyCells={handleApplyCells}
            onHoverSlot={setHoveredSlot}
            shiftHours={shiftHours}
            sectionTitle="Your Availability"
            sectionSub="Drag or click along boxes to indicate your free hours"
          />

          <section className="availability-section availability-section--team" ref={teamSectionRef}>
            <AvailabilityGrid
              mode="team"
              weekNumber={activeWeek}
              availableSlots={myAvailability}
              overlapCount={overlapCount}
              teamSize={activeMembers.length}
              meetings={meetings}
              activeMeetingId={editingMeetingId}
              selection={editingMeetingId ? null : selection}
              onHoverSlot={setHoveredSlot}
              onSelectionStart={handleSelectionStart}
              onSelectionComplete={handleSelectionComplete}
              onMeetingClick={handleMeetingClick}
              shiftHours={shiftHours}
              sectionTitle="Team Availability"
              sectionSub="Drag or click boxes to suggest a meeting time"
            />
            {proposal && (
              <MeetingProposalCard
                mode={editingMeetingId ? 'edit' : 'new'}
                weekNumber={activeWeek}
                startDay={proposal.startDay}
                startHour={proposal.startHour}
                endDay={proposal.endDay}
                endHour={proposal.endHour}
                subject={proposal.subject}
                onSubjectChange={(v) => setProposal((p) => ({ ...p, subject: v }))}
                onStartChange={(d, h) =>
                  setProposal((p) => ({ ...p, startDay: d, startHour: h }))
                }
                onEndChange={(d, h) =>
                  setProposal((p) => ({ ...p, endDay: d, endHour: h }))
                }
                onSave={handleSaveProposal}
                onDiscard={closeProposal}
                onDelete={handleDeleteProposal}
                style={proposalStyle}
              />
            )}
          </section>
        </div>

        <aside className="availability-sidebar">
          <div className="availability-sidebar-section">
            <div className="availability-sidebar-heading">{hoverHeading}</div>
            {hoveredNames.length > 0 ? (
              <ul className="availability-name-list">
                {hoveredNames.map((name) => (
                  <li key={name} className="availability-name">
                    {name}
                  </li>
                ))}
              </ul>
            ) : (
              <p className="availability-sidebar-empty">No one available</p>
            )}
          </div>
          <div className="availability-sidebar-section">
            <div className="availability-sidebar-heading">Upcoming Meetings</div>
            {weekMeetings.length > 0 ? (
              <ul className="availability-meeting-list">
                {weekMeetings.map((m) => (
                  <li key={m.id} className="availability-meeting-item">
                    <button
                      type="button"
                      className="availability-meeting-btn"
                      onClick={() => handleMeetingClick(m)}
                    >
                      <span className="availability-meeting-time">
                        {formatMeetingRange(m, activeWeek)}
                      </span>
                      <span className="availability-meeting-subject">{m.subject}</span>
                    </button>
                  </li>
                ))}
              </ul>
            ) : (
              <p className="availability-sidebar-empty">Nothing&apos;s planned :)</p>
            )}
          </div>
        </aside>
      </div>
    </div>
  );
}
