import type { ReactiveController, ReactiveControllerHost } from "lit";
import type { GatewayBrowserClient } from "../api/gateway.ts";
import type { ApplicationGateway } from "../app/gateway.ts";
import { isGatewayMethodAdvertised } from "../lib/gateway-methods.ts";
import {
  scopedSessionPullRequestKey,
  SESSION_PULL_REQUESTS_SUBSCRIBE_METHOD,
  sessionPullRequestsForGateway,
  type SessionPullRequestSnapshotStore,
} from "../lib/session-pull-requests.ts";
import { parseAgentSessionKey } from "../lib/sessions/session-key.ts";
import type { SidebarRecentSession } from "./app-sidebar-session-types.ts";
import {
  resolveSessionPullRequestIndicatorState,
  type SessionPullRequestIndicatorState,
} from "./session-menu-work.ts";

type IndicatorEntry = {
  state: SessionPullRequestIndicatorState;
  worktreeId: string;
};

type SessionPullRequestIndicatorsOptions = {
  getConnected: () => boolean;
  getRows: () => readonly SidebarRecentSession[];
  getSelectedAgentId: () => string;
  getGateway: () => ApplicationGateway | undefined;
};

/** Projects pushed PR snapshots for the currently visible worktree rows. */
export class SessionPullRequestIndicatorsController implements ReactiveController {
  private readonly states = new Map<string, IndicatorEntry>();
  private gateway: ApplicationGateway | null = null;
  private client: GatewayBrowserClient | null = null;
  private agentId: string | null = null;
  private store: SessionPullRequestSnapshotStore | null = null;
  private stopStoreUpdates: (() => void) | null = null;
  private connected = false;
  private refreshScheduled = false;

  constructor(
    private readonly host: ReactiveControllerHost,
    private readonly options: SessionPullRequestIndicatorsOptions,
  ) {
    host.addController(this);
  }

  hostConnected(): void {
    this.connected = true;
  }

  hostUpdated(): void {
    this.scheduleRefresh();
  }

  hostDisconnected(): void {
    this.connected = false;
    this.releaseStore();
    this.reset(false);
  }

  state(sessionKey: string, worktreeId: string): SessionPullRequestIndicatorState {
    const entry = this.states.get(sessionKey);
    return entry?.worktreeId === worktreeId ? entry.state : "none";
  }

  private scheduleRefresh(): void {
    if (this.refreshScheduled) {
      return;
    }
    this.refreshScheduled = true;
    globalThis.setTimeout(() => {
      this.refreshScheduled = false;
      if (this.connected) {
        this.refreshVisible();
      }
    }, 0);
  }

  private releaseStore(): void {
    this.store?.unwatch(this);
    this.stopStoreUpdates?.();
    this.stopStoreUpdates = null;
    this.store = null;
    this.gateway = null;
    this.client = null;
    this.agentId = null;
  }

  private reset(requestUpdate: boolean): void {
    if (this.states.size === 0) {
      return;
    }
    this.states.clear();
    if (requestUpdate) {
      this.host.requestUpdate();
    }
  }

  private eligibleRows(): readonly SidebarRecentSession[] {
    return this.options.getRows().filter((session) => !session.isChild && session.worktreeId);
  }

  private scopedKey(sessionKey: string): string {
    return scopedSessionPullRequestKey(
      sessionKey,
      parseAgentSessionKey(sessionKey)?.agentId ?? this.options.getSelectedAgentId(),
    );
  }

  private applySnapshots(): void {
    const store = this.store;
    if (!store) {
      return;
    }
    let changed = false;
    for (const session of this.eligibleRows()) {
      if (!session.worktreeId) {
        continue;
      }
      const snapshot = store.get(this.scopedKey(session.key));
      // Empty failure snapshots retain the rendered chip; snapshots carrying
      // last-known PRs can also hydrate a newly mounted row.
      if (!snapshot || (snapshot.status !== "ready" && snapshot.pullRequests.length === 0)) {
        continue;
      }
      const entry = {
        state: resolveSessionPullRequestIndicatorState(snapshot.pullRequests),
        worktreeId: session.worktreeId,
      };
      const current = this.states.get(session.key);
      if (current?.state !== entry.state || current.worktreeId !== entry.worktreeId) {
        this.states.set(session.key, entry);
        changed = true;
      }
    }
    if (changed) {
      this.host.requestUpdate();
    }
  }

  private refreshVisible(): void {
    const gateway = this.options.getGateway();
    if (
      !gateway ||
      !this.options.getConnected() ||
      isGatewayMethodAdvertised(gateway.snapshot, SESSION_PULL_REQUESTS_SUBSCRIBE_METHOD) !== true
    ) {
      this.releaseStore();
      this.reset(true);
      return;
    }
    if (gateway !== this.gateway) {
      this.releaseStore();
      this.gateway = gateway;
      this.store = sessionPullRequestsForGateway(gateway);
      this.stopStoreUpdates = this.store.subscribe(() => this.applySnapshots());
    }
    if (gateway.snapshot.client !== this.client) {
      this.client = gateway.snapshot.client;
      this.reset(true);
    }
    const selectedAgentId = this.options.getSelectedAgentId();
    if (selectedAgentId !== this.agentId) {
      this.agentId = selectedAgentId;
      this.reset(true);
    }

    const eligibleRows = this.eligibleRows();
    const eligibleKeys = new Set(eligibleRows.map((session) => session.key));
    let removed = false;
    for (const sessionKey of this.states.keys()) {
      if (!eligibleKeys.has(sessionKey)) {
        this.states.delete(sessionKey);
        removed = true;
      }
    }
    if (removed) {
      this.host.requestUpdate();
    }
    this.store?.watch(
      this,
      eligibleRows.map((session) => this.scopedKey(session.key)),
    );
    this.applySnapshots();
  }
}
