import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { ref, createRef } from 'lit/directives/ref.js';
import WaSlider from '@awesome.me/webawesome/dist/components/slider/slider.js';
import { formatSeconds } from '@utils/time';
import { PlayerController } from '@store/controllers/player-controller';
import { designTokens } from '../../../styles/tokens.css';
import { waSliderLabel } from '../../../styles/wa-slider-label.css';

const ProgressIntervalMillis = 1000;

@customElement('seek-bar')
export class SeekBar extends LitElement {
  private player = new PlayerController(this);
  private rangeRef = createRef<WaSlider>();
  private timerID: number = -1;
  private previousTrackChangeId: number = -1;

  /** The sequence number of the last backend report applied. */
  private previousPositionSeq: number = -1;

  @state()
  private seekValue: number = 0;

  /** Whether the right-hand clock shows time remaining or total. */
  @state()
  private showRemaining: boolean = true;

  static override styles = [designTokens, waSliderLabel, css`
    /* 12px below the phone breakpoint. The bottom bar's seek bar is
       display:none there (016 B2 phase 1), so the only instance a
       viewport media query can reach at that width is the full-screen
       now-playing view's -- which is exactly the one a thumb uses.
       The track size lives on wa-slider inside this shadow root, so a
       custom property set by the host would not reach it. */
    @media (max-width: 599px) {
      wa-slider {
        --track-size: 12px;
      }
    }

    wa-slider {
      --track-size: 6px;
      flex: 1;
      margin: 0 16px;
      --wa-tooltip-background-color: var(--yj-bg-elevated, #343a40);
      --wa-tooltip-content-color: var(--yj-text-primary, white);
      --wa-tooltip-border-color: var(--yj-bg-elevated, #343a40);
      --wa-tooltip-border-radius: 4px;
      --wa-tooltip-font-size: var(--yj-text-lg);
    }

    wa-slider::part(track) {
      background: var(--yj-text-primary, white);
    }

    wa-slider::part(indicator) {
      background: var(--yj-accent, yellow);
    }

    wa-slider::part(thumb) {
      background: var(--yj-bg-base, black);
    }

    #seek-bar-container {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    /* The clocks must not resize as they count.

       Two things move them, and they need different answers. Digits in
       a proportional font are different widths, so 1:11 is narrower
       than 4:08 and the bar breathed once a second -- that is what
       tabular figures fix. The character *count* changes too, at the
       hundredth minute and whenever the right-hand clock is toggled to
       remaining and grows a minus sign, and a figure width cannot fix
       that -- so each clock also reserves the widest string this track
       can put in it. The budget is per track rather than a constant
       because reserving six characters on every track would push the
       slider in by a character at each end for nothing. */
    #seek-bar-container small,
    .time-toggle {
      font-variant-numeric: tabular-nums;
      flex: 0 0 auto;
      min-width: calc(var(--yj-clock-chars, 5) * 1ch);
    }

    #seek-bar-container small {
      text-align: left;
    }

    .time-toggle {
      background: none;
      border: none;
      padding: 0;
      color: inherit;
      font: inherit;
      font-size: var(--wa-font-size-s, 0.875rem);
      cursor: pointer;
      /* One more for the minus sign the remaining form carries. */
      min-width: calc((var(--yj-clock-chars, 5) + 1) * 1ch);
      text-align: right;
    }

    .time-toggle:hover,
    .time-toggle:focus-visible {
      text-decoration: underline;
    }
  `];

  // ===================================================================
  // DERIVED STATE
  // ===================================================================

  private get hasTrack(): boolean {
    return this.player.currentTrack !== null;
  }

  private get trackLength(): number {
    return this.player.currentTrack?.trackLength ?? 0;
  }

  private get isPlaying(): boolean {
    return this.player.isPlaying;
  }

  // ===================================================================
  // LIFECYCLE
  // ===================================================================

  override disconnectedCallback() {
    super.disconnectedCallback();
    this.stopProgress();
  }

  override updated() {
    // Detect track change and reset seek position.
    // Uses trackChangeId instead of filePath so the seek bar resets
    // even when the same file plays consecutively in the queue.
    const currentChangeId = this.player.currentTrack?.trackChangeId ?? -1;

    if (currentChangeId !== this.previousTrackChangeId) {
      this.previousTrackChangeId = currentChangeId;
      this.seekValue = this.player.currentTrack?.seekPosition ?? 0;
      this.stopProgress();
    }

    // The backend's own position wins over anything counted here.
    // Every report resets the interpolation, so the bar can be at most
    // one tick wrong and can never accumulate — which is what made a
    // keyboard seek desync it by 30 s (H-3).
    // A report for a track that is no longer loaded is stale by
    // definition: the change id is the only thing that distinguishes
    // it, since the same file can play twice in a row.
    const position = this.player.position;
    const forThisTrack =
      position !== null && position.trackChangeId === currentChangeId;

    if (position && forThisTrack && position.seq !== this.previousPositionSeq) {
      this.previousPositionSeq = position.seq;
      this.seekValue = position.positionSeconds;
      this.stopProgress();
    }

    // Start/stop progress interval based on playback state
    if (this.isPlaying && this.hasTrack) {
      this.startProgress();
    } else {
      this.stopProgress();
    }
  }

  // ===================================================================
  // PROGRESS INTERVAL
  // ===================================================================

  private stopProgress() {
    if (this.timerID !== -1) {
      clearInterval(this.timerID);
      this.timerID = -1;
    }
  }

  /**
   * Interpolate between backend reports.
   *
   * This is not the clock — it exists only so the display moves
   * smoothly in the second between two ticks.  It is stopped and
   * restarted by every report, so its error is bounded by one second
   * and is discarded rather than carried.
   */
  private startProgress() {
    // Don't start multiple intervals
    if (this.timerID !== -1) {
      return;
    }

    this.timerID = window.setInterval(() => {
      if (this.seekValue < this.trackLength) {
        this.seekValue += 1;
      }
    }, ProgressIntervalMillis);
  }

  // ===================================================================
  // EVENT HANDLERS
  // ===================================================================

  private handleChange(e: Event) {
    const newSeekVal = (e.target as WaSlider).value;
    this.setSeekValue(newSeekVal);
    this.player.seek(newSeekVal);

    if (this.isPlaying) {
      this.startProgress();
    }
  }

  // Stops progress while user is dragging the thumb
  private handleInput() {
    this.stopProgress();
  }

  private setSeekValue(val: number) {
    if (val < 0) val = 0;
    if (val > this.trackLength) val = this.trackLength;
    this.seekValue = val;
  }

  // ===================================================================
  // RENDER
  // ===================================================================

  /** H-16: the right-hand clock never said which number it was. */
  private toggleRemaining() {
    this.showRemaining = !this.showRemaining;
  }

  override render() {
    const elapsedTime = this.hasTrack ? formatSeconds(this.seekValue) : '--:--';
    const rightLabel = this.showRemaining
      ? `-${formatSeconds(Math.max(0, this.trackLength - this.seekValue))}`
      : formatSeconds(this.trackLength);
    const rightTime = this.hasTrack ? rightLabel : '--:--';

    // The widest string either clock can hold for *this* track. The
    // duration is the longest elapsed value there can be, so its length
    // is the budget; `--:--` is five, which is also the floor.
    const clockChars = Math.max(
      5,
      this.hasTrack ? formatSeconds(this.trackLength).length : 0,
    );

    return html`
      <div
        id="seek-bar-container"
        style="--yj-clock-chars: ${clockChars}"
      >
        <small data-testid="elapsed-time">${elapsedTime}</small>
        <wa-slider
          label="Seek"
          .value="${this.seekValue}"
          max="${this.trackLength}"
          ?with-tooltip="${this.hasTrack}"
          .valueFormatter="${this.hasTrack ? formatSeconds : null}"
          ${ref(this.rangeRef)}
          @change="${this.handleChange}"
          @input="${this.handleInput}"
        ></wa-slider>
        <button
          class="time-toggle"
          type="button"
          data-testid="remaining-time"
          title="${this.showRemaining
            ? 'Time remaining — click for total duration'
            : 'Total duration — click for time remaining'}"
          aria-label="${this.showRemaining
            ? `Time remaining ${formatSeconds(
                Math.max(0, this.trackLength - this.seekValue),
              )}. Show total duration.`
            : `Total duration ${formatSeconds(
                this.trackLength,
              )}. Show time remaining.`}"
          @click="${this.toggleRemaining}"
        >${rightTime}</button>
      </div>
    `;
  }
}
