import { ICalendarEvent, IEventBus, IEventUpdatedPayload } from '../../types/CalendarTypes';
import { EventService } from '../../storage/events/EventService';
import { DateService } from '../../core/DateService';
import { IGridConfig } from '../../core/IGridConfig';
import { calculateEventPosition, snapToGrid, pixelsToMinutes } from '../../utils/PositionUtils';
import { CoreEvents } from '../../constants/CoreEvents';
import { IDragColumnChangePayload, IDragMovePayload, IDragEndPayload, IDragLeaveHeaderPayload } from '../../types/DragTypes';
import { calculateColumnLayout } from './EventLayoutEngine';
import { IGridGroupLayout } from './EventLayoutTypes';
import { FilterTemplate } from '../../core/FilterTemplate';

/**
 * EventRenderer - Renders calendar events to the DOM
 *
 * CLEAN approach:
 * - Only data-id attribute on event element
 * - innerHTML contains only visible content
 * - Event data retrieved via EventService when needed
 */
export class EventRenderer {
  private container: HTMLElement | null = null;

  constructor(
    private eventService: EventService,
    private dateService: DateService,
    private gridConfig: IGridConfig,
    private eventBus: IEventBus
  ) {
    this.setupListeners();
  }

  /**
   * Setup listeners for drag-drop and update events
   */
  private setupListeners(): void {
    this.eventBus.on(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, (e) => {
      const payload = (e as CustomEvent<IDragColumnChangePayload>).detail;
      this.handleColumnChange(payload);
    });

    this.eventBus.on(CoreEvents.EVENT_DRAG_MOVE, (e) => {
      const payload = (e as CustomEvent<IDragMovePayload>).detail;
      this.updateDragTimestamp(payload);
    });

    this.eventBus.on(CoreEvents.EVENT_UPDATED, (e) => {
      const payload = (e as CustomEvent<IEventUpdatedPayload>).detail;
      this.handleEventUpdated(payload);
    });

    this.eventBus.on(CoreEvents.EVENT_DRAG_END, (e) => {
      const payload = (e as CustomEvent<IDragEndPayload>).detail;
      this.handleDragEnd(payload);
    });

    this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => {
      const payload = (e as CustomEvent<IDragLeaveHeaderPayload>).detail;
      this.handleDragLeaveHeader(payload);
    });
  }

  /**
   * Handle EVENT_DRAG_END - remove element if dropped in header
   */
  private handleDragEnd(payload: IDragEndPayload): void {
    if (payload.target === 'header') {
      // Event was dropped in header drawer - remove from grid
      const element = this.container?.querySelector(`swp-content-viewport swp-event[data-event-id="${payload.swpEvent.eventId}"]`);
      element?.remove();
    }
  }

  /**
   * Handle header item leaving header - create swp-event in grid
   */
  private handleDragLeaveHeader(payload: IDragLeaveHeaderPayload): void {
    // Only handle when source is header (header item dragged to grid)
    if (payload.source !== 'header') return;
    if (!payload.targetColumn || !payload.start || !payload.end) return;

    // Turn header item into ghost (stays visible but faded)
    if (payload.element) {
      payload.element.classList.add('drag-ghost');
      payload.element.style.opacity = '0.3';
      payload.element.style.pointerEvents = 'none';
    }

    // Create event object from header item data
    const event: ICalendarEvent = {
      id: payload.eventId,
      title: payload.title || '',
      description: '',
      start: payload.start,
      end: payload.end,
      type: 'customer',
      allDay: false,
      syncStatus: 'pending'
    };

    // Create swp-event element using existing method
    const element = this.createEventElement(event);

    // Add to target column
    let eventsLayer = payload.targetColumn.querySelector('swp-events-layer');
    if (!eventsLayer) {
      eventsLayer = document.createElement('swp-events-layer');
      payload.targetColumn.appendChild(eventsLayer);
    }
    eventsLayer.appendChild(element);

    // Mark as dragging so DragDropManager can continue with it
    element.classList.add('dragging');
  }

  /**
   * Handle EVENT_UPDATED - re-render affected columns
   */
  private async handleEventUpdated(payload: IEventUpdatedPayload): Promise<void> {
    // Re-render source column (if different from target)
    if (payload.sourceColumnKey !== payload.targetColumnKey) {
      await this.rerenderColumn(payload.sourceColumnKey);
    }

    // Re-render target column
    await this.rerenderColumn(payload.targetColumnKey);
  }

  /**
   * Re-render a single column with fresh data from IndexedDB
   */
  private async rerenderColumn(columnKey: string): Promise<void> {
    const column = this.findColumn(columnKey);
    if (!column) return;

    // Read date and resourceId directly from column attributes (columnKey is opaque)
    const date = column.dataset.date;
    const resourceId = column.dataset.resourceId;

    if (!date) return;

    // Get date range for this day
    const startDate = new Date(date);
    const endDate = new Date(date);
    endDate.setHours(23, 59, 59, 999);

    // Fetch events from IndexedDB
    const events = resourceId
      ? await this.eventService.getByResourceAndDateRange(resourceId, startDate, endDate)
      : await this.eventService.getByDateRange(startDate, endDate);

    // Filter to timed events and match date exactly
    const timedEvents = events.filter(event =>
      !event.allDay && this.dateService.getDateKey(event.start) === date
    );

    // Get or create events layer
    let eventsLayer = column.querySelector('swp-events-layer');
    if (!eventsLayer) {
      eventsLayer = document.createElement('swp-events-layer');
      column.appendChild(eventsLayer);
    }

    // Clear existing events
    eventsLayer.innerHTML = '';

    // Calculate layout with stacking/grouping
    const layout = calculateColumnLayout(timedEvents, this.gridConfig);

    // Render GRID groups
    layout.grids.forEach(grid => {
      const groupEl = this.renderGridGroup(grid);
      eventsLayer!.appendChild(groupEl);
    });

    // Render STACKED events
    layout.stacked.forEach(item => {
      const eventEl = this.renderStackedEvent(item.event, item.stackLevel);
      eventsLayer!.appendChild(eventEl);
    });
  }

  /**
   * Find a column element by columnKey
   */
  private findColumn(columnKey: string): HTMLElement | null {
    if (!this.container) return null;
    return this.container.querySelector(`swp-day-column[data-column-key="${columnKey}"]`) as HTMLElement;
  }

  /**
   * Handle event moving to a new column during drag
   */
  private handleColumnChange(payload: IDragColumnChangePayload): void {
    const eventsLayer = payload.newColumn.querySelector('swp-events-layer');
    if (!eventsLayer) return;

    // Move element to new column
    eventsLayer.appendChild(payload.element);

    // Preserve Y position
    payload.element.style.top = `${payload.currentY}px`;
  }

  /**
   * Update timestamp display during drag (snapped to grid)
   */
  private updateDragTimestamp(payload: IDragMovePayload): void {
    const timeEl = payload.element.querySelector('swp-event-time');
    if (!timeEl) return;

    // Snap position to grid interval
    const snappedY = snapToGrid(payload.currentY, this.gridConfig);

    // Calculate new start time
    const minutesFromGridStart = pixelsToMinutes(snappedY, this.gridConfig);
    const startMinutes = (this.gridConfig.dayStartHour * 60) + minutesFromGridStart;

    // Keep original duration (from element height)
    const height = parseFloat(payload.element.style.height) || this.gridConfig.hourHeight;
    const durationMinutes = pixelsToMinutes(height, this.gridConfig);

    // Create Date objects for consistent formatting via DateService
    const start = this.minutesToDate(startMinutes);
    const end = this.minutesToDate(startMinutes + durationMinutes);

    timeEl.textContent = this.dateService.formatTimeRange(start, end);
  }

  /**
   * Convert minutes since midnight to a Date object (today)
   */
  private minutesToDate(minutes: number): Date {
    const date = new Date();
    date.setHours(Math.floor(minutes / 60) % 24, minutes % 60, 0, 0);
    return date;
  }

  /**
   * Render events for visible dates into day columns
   * @param container - Calendar container element
   * @param filter - Filter with 'date' and optionally 'resource' arrays
   * @param filterTemplate - Template for matching events to columns
   */
  async render(container: HTMLElement, filter: Record<string, string[]>, filterTemplate: FilterTemplate): Promise<void> {
    // Store container reference for later re-renders
    this.container = container;

    const visibleDates = filter['date'] || [];

    if (visibleDates.length === 0) return;

    // Get date range for query
    const startDate = new Date(visibleDates[0]);
    const endDate = new Date(visibleDates[visibleDates.length - 1]);
    endDate.setHours(23, 59, 59, 999);

    // Fetch events from IndexedDB
    const events = await this.eventService.getByDateRange(startDate, endDate);

    // Find day columns
    const dayColumns = container.querySelector('swp-day-columns');
    if (!dayColumns) return;

    const columns = dayColumns.querySelectorAll('swp-day-column');

    // Render events into each column based on FilterTemplate matching
    columns.forEach(column => {
      const columnEl = column as HTMLElement;

      // Use FilterTemplate for matching - only fields in template are checked
      const columnEvents = events.filter(event => filterTemplate.matches(event, columnEl));

      // Get or create events layer
      let eventsLayer = column.querySelector('swp-events-layer');
      if (!eventsLayer) {
        eventsLayer = document.createElement('swp-events-layer');
        column.appendChild(eventsLayer);
      }

      // Clear existing events
      eventsLayer.innerHTML = '';

      // Filter to timed events only
      const timedEvents = columnEvents.filter(event => !event.allDay);

      // Calculate layout with stacking/grouping
      const layout = calculateColumnLayout(timedEvents, this.gridConfig);

      // Render GRID groups (simultaneous events side-by-side)
      layout.grids.forEach(grid => {
        const groupEl = this.renderGridGroup(grid);
        eventsLayer!.appendChild(groupEl);
      });

      // Render STACKED events (overlapping with margin offset)
      layout.stacked.forEach(item => {
        const eventEl = this.renderStackedEvent(item.event, item.stackLevel);
        eventsLayer!.appendChild(eventEl);
      });
    });
  }

  /**
   * Create a single event element
   *
   * CLEAN approach:
   * - Only data-id for lookup
   * - Visible content in innerHTML only
   */
  private createEventElement(event: ICalendarEvent): HTMLElement {
    const element = document.createElement('swp-event');

    // Data attributes for SwpEvent compatibility
    element.dataset.eventId = event.id;
    if (event.resourceId) {
      element.dataset.resourceId = event.resourceId;
    }

    // Calculate position
    const position = calculateEventPosition(event.start, event.end, this.gridConfig);
    element.style.top = `${position.top}px`;
    element.style.height = `${position.height}px`;

    // Color class based on event type
    const colorClass = this.getColorClass(event);
    if (colorClass) {
      element.classList.add(colorClass);
    }

    // Visible content only
    element.innerHTML = `
      <swp-event-time>${this.dateService.formatTimeRange(event.start, event.end)}</swp-event-time>
      <swp-event-title>${this.escapeHtml(event.title)}</swp-event-title>
      ${event.description ? `<swp-event-description>${this.escapeHtml(event.description)}</swp-event-description>` : ''}
    `;

    return element;
  }

  /**
   * Get color class based on metadata.color or event type
   */
  private getColorClass(event: ICalendarEvent): string {
    // Check metadata.color first
    if (event.metadata?.color) {
      return `is-${event.metadata.color}`;
    }

    // Fallback to type-based color
    const typeColors: Record<string, string> = {
      'customer': 'is-blue',
      'vacation': 'is-green',
      'break': 'is-amber',
      'meeting': 'is-purple',
      'blocked': 'is-red'
    };
    return typeColors[event.type] || 'is-blue';
  }

  /**
   * Escape HTML to prevent XSS
   */
  private escapeHtml(text: string): string {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }

  /**
   * Render a GRID group with side-by-side columns
   * Used when multiple events start at the same time
   */
  private renderGridGroup(layout: IGridGroupLayout): HTMLElement {
    const group = document.createElement('swp-event-group');
    group.classList.add(`cols-${layout.columns.length}`);
    group.style.top = `${layout.position.top}px`;

    // Stack level styling for entire group (if nested in another event)
    if (layout.stackLevel > 0) {
      group.style.marginLeft = `${layout.stackLevel * 15}px`;
      group.style.zIndex = `${100 + layout.stackLevel}`;
    }

    // Calculate the height needed for the group (tallest event)
    let maxBottom = 0;
    for (const event of layout.events) {
      const pos = calculateEventPosition(event.start, event.end, this.gridConfig);
      const eventBottom = pos.top + pos.height;
      if (eventBottom > maxBottom) maxBottom = eventBottom;
    }
    const groupHeight = maxBottom - layout.position.top;
    group.style.height = `${groupHeight}px`;

    // Create wrapper div for each column
    layout.columns.forEach(columnEvents => {
      const wrapper = document.createElement('div');
      wrapper.style.position = 'relative';

      columnEvents.forEach(event => {
        const eventEl = this.createEventElement(event);
        // Position relative to group top
        const pos = calculateEventPosition(event.start, event.end, this.gridConfig);
        eventEl.style.top = `${pos.top - layout.position.top}px`;
        eventEl.style.position = 'absolute';
        eventEl.style.left = '0';
        eventEl.style.right = '0';
        wrapper.appendChild(eventEl);
      });

      group.appendChild(wrapper);
    });

    return group;
  }

  /**
   * Render a STACKED event with margin-left offset
   * Used for overlapping events that don't start at the same time
   */
  private renderStackedEvent(event: ICalendarEvent, stackLevel: number): HTMLElement {
    const element = this.createEventElement(event);

    // Add stack metadata for drag-drop and other features
    element.dataset.stackLink = JSON.stringify({ stackLevel });

    // Visual styling based on stack level
    if (stackLevel > 0) {
      element.style.marginLeft = `${stackLevel * 15}px`;
      element.style.zIndex = `${100 + stackLevel}`;
    }

    return element;
  }
}
