import fs from "node:fs/promises";
import path from "node:path";

export type FeedSubscription = {
  username: string;
  label?: string;
  agentId?: string;
  addedAtMs: number;
  pollIntervalMs?: number;
  deliveryChannel?: string;
  deliveryTo?: string;
  filter?: string;
  enabled: boolean;
  source: "config" | "tool";
};

export type FeedPollState = {
  lastPollAtMs: number;
  lastItemId?: string;
  lastItemPubDateMs?: number;
  seenIds: string[];
  consecutiveErrors: number;
  lastError?: string;
};

export type NitterFeedStateFile = {
  version: 1;
  subscriptions: FeedSubscription[];
  pollState: Record<string, FeedPollState>;
};

const MAX_SEEN_IDS = 200;

function defaultState(): NitterFeedStateFile {
  return { version: 1, subscriptions: [], pollState: {} };
}

function defaultPollState(): FeedPollState {
  return { lastPollAtMs: 0, seenIds: [], consecutiveErrors: 0 };
}

function keyOf(username: string): string {
  return username.toLowerCase();
}

export class FeedStateManager {
  private readonly filePath: string;
  private state: NitterFeedStateFile = defaultState();
  private savePromise: Promise<void> | null = null;

  constructor(stateDir: string) {
    this.filePath = path.join(stateDir, "plugins", "nitter", "feed-state.json");
  }

  async load(): Promise<void> {
    try {
      const raw = await fs.readFile(this.filePath, "utf-8");
      const parsed = JSON.parse(raw) as Partial<NitterFeedStateFile>;

      if (parsed?.version === 1 && Array.isArray(parsed.subscriptions)) {
        this.state = {
          version: 1,
          subscriptions: parsed.subscriptions,
          pollState:
            parsed.pollState && typeof parsed.pollState === "object" ? parsed.pollState : {},
        };
      } else {
        this.state = defaultState();
      }
    } catch {
      this.state = defaultState();
    }
  }

  async save(): Promise<void> {
    const dir = path.dirname(this.filePath);
    await fs.mkdir(dir, { recursive: true });
    await fs.writeFile(this.filePath, JSON.stringify(this.state, null, 2) + "\n", "utf-8");
  }

  addSubscription(sub: Omit<FeedSubscription, "addedAtMs">): FeedSubscription {
    const existing = this.state.subscriptions.find(
      (s) => keyOf(s.username) === keyOf(sub.username),
    );
    if (existing) {
      return existing;
    }

    const full: FeedSubscription = { ...sub, addedAtMs: Date.now() };
    this.state.subscriptions.push(full);
    void this.save();
    return full;
  }

  removeSubscription(username: string): boolean {
    const key = keyOf(username);
    const idx = this.state.subscriptions.findIndex((s) => keyOf(s.username) === key);
    if (idx === -1) return false;

    this.state.subscriptions.splice(idx, 1);
    delete this.state.pollState[key];
    void this.save();
    return true;
  }

  listSubscriptions(): FeedSubscription[] {
    return this.state.subscriptions;
  }

  getSubscription(username: string): FeedSubscription | undefined {
    return this.state.subscriptions.find((s) => keyOf(s.username) === keyOf(username));
  }

  getPollState(username: string): FeedPollState {
    const key = keyOf(username);
    return this.state.pollState[key] ?? defaultPollState();
  }

  recordPoll(
    username: string,
    result: { items?: Array<{ id: string }>; error?: string },
  ): Array<{ id: string }> {
    const key = keyOf(username);
    const ps = this.state.pollState[key] ?? defaultPollState();

    ps.lastPollAtMs = Date.now();

    if (result.error) {
      ps.consecutiveErrors += 1;
      ps.lastError = result.error;
      this.state.pollState[key] = ps;
      void this.save();
      return [];
    }

    // Success path — reset error tracking
    ps.consecutiveErrors = 0;
    ps.lastError = undefined;

    const items = result.items ?? [];
    const seenSet = new Set(ps.seenIds);
    const newItems = items.filter((item) => !seenSet.has(item.id));

    for (const item of newItems) {
      ps.seenIds.push(item.id);
    }

    // Cap seenIds at MAX_SEEN_IDS (FIFO — drop oldest)
    if (ps.seenIds.length > MAX_SEEN_IDS) {
      ps.seenIds = ps.seenIds.slice(ps.seenIds.length - MAX_SEEN_IDS);
    }

    if (newItems.length > 0) {
      ps.lastItemId = newItems[newItems.length - 1].id;
    }

    this.state.pollState[key] = ps;
    void this.save();
    return newItems;
  }

  syncFromConfig(
    configFeeds: Array<{
      username: string;
      label?: string;
      agentId?: string;
      pollIntervalMs?: number;
      deliveryChannel?: string;
      deliveryTo?: string;
      filter?: string;
      enabled: boolean;
    }>,
  ): void {
    const configKeys = new Set(configFeeds.map((f) => keyOf(f.username)));

    // Remove config-sourced subscriptions that are no longer in config
    this.state.subscriptions = this.state.subscriptions.filter((sub) => {
      if (sub.source === "tool") return true;
      return configKeys.has(keyOf(sub.username));
    });

    // Upsert config feeds
    for (const feed of configFeeds) {
      const key = keyOf(feed.username);
      const idx = this.state.subscriptions.findIndex((s) => keyOf(s.username) === key);

      const entry: FeedSubscription = {
        username: feed.username,
        label: feed.label,
        agentId: feed.agentId,
        pollIntervalMs: feed.pollIntervalMs,
        deliveryChannel: feed.deliveryChannel,
        deliveryTo: feed.deliveryTo,
        filter: feed.filter,
        enabled: feed.enabled,
        addedAtMs: idx !== -1 ? this.state.subscriptions[idx].addedAtMs : Date.now(),
        source: "config",
      };

      if (idx !== -1) {
        this.state.subscriptions[idx] = entry;
      } else {
        this.state.subscriptions.push(entry);
      }
    }

    void this.save();
  }
}
