/**
 * CalDAV broker poller — runs every 5 minutes via setInterval.
 *
 * Responsibilities (D-13, D-02):
 *   - Load all member_credentials (N-credential per-member model)
 *   - Decrypt each app password via decryptPassword (T-03-04 — never log the decrypted value)
 *   - Create a tsdav client per credential, fetch calendars via PROPFIND
 *   - For each calendar: compare current ctag to stored ctag
 *       → ctag unchanged (and non-null): SKIP (no DB write, no Fastmail round-trip)
 *       → ctag changed or null: call syncCalendar (REPORT → ical.js → DB upsert)
 *
 * runPoll is exported for unit testing (inject mocks via vi.mock at the module level).
 * startBrokerPoller wraps it in a 5-minute setInterval.
 * (node-cron 4.2.1 silently skipped scheduled executions in the long-running server process;
 * setInterval fires reliably in the same process — replaced to fix the silent skip.)
 */
import { and, eq } from 'drizzle-orm';
import { db } from '../db/client.js';
import { memberCredentials, calendars } from '../db/schema.js';
import { decryptPassword } from './crypto.js';
import { createFastmailClient } from './client.js';
import { syncCalendar } from './sync.js';
import { dispatchEventChange } from '../lib/eventChangeDispatcher.js';

/**
 * Runs one full poll cycle:
 *   1. Load all member credentials
 *   2. For each credential: decrypt, create client, fetch calendars
 *   3. For each calendar: check ctag; skip if unchanged, sync if changed
 *
 * Errors for individual credentials are caught and logged (not re-thrown),
 * so one bad credential does not stop processing for others.
 */
export async function runPoll(): Promise<void> {
  const creds = await db.select().from(memberCredentials);

  for (const cred of creds) {
    try {
      // Decrypt before client creation (T-03-04: never expose decrypted value in logs)
      const appPassword = decryptPassword(cred.encryptedPassword);

      const client = await createFastmailClient(cred.fastmailEmail, appPassword);
      const davCalendars = await client.fetchCalendars();

      for (const davCal of davCalendars) {
        // Look up the stored calendar row to get the known ctag (D-13).
        // BUG B: scope by (userId, url). The two members share one Fastmail account
        // (D-16), so the same collection URL exists for both. A url-only predicate
        // matched the OTHER member's row → wrong ctag compared, and syncCalendar
        // wrote events under the wrong calendar. Match on this member's row only.
        const [stored] = await db
          .select()
          .from(calendars)
          .where(and(eq(calendars.userId, cred.userId), eq(calendars.url, davCal.url)))
          .limit(1);

        // ctag/syncToken: defensive null handling (Pitfall #6)
        const knownCtag = stored?.ctag ?? null;
        const currentCtag = davCal.ctag ?? davCal.syncToken ?? null;

        // Skip if ctag is present on both sides and unchanged
        if (currentCtag !== null && currentCtag === knownCtag) {
          continue;
        }

        // NOTIF-03: pass onChanges so external event changes push to non-actor members.
        // actor = cred.userId (the member whose Fastmail poll detected the change — D-03).
        await syncCalendar(client, davCal, cred.userId, (changes) => {
          for (const change of changes) {
            dispatchEventChange(change, cred.userId).catch((err: unknown) => {
              console.error(
                '[broker/poller] dispatchEventChange error:',
                err instanceof Error ? err.message : String(err),
              );
            });
          }
        });
      }
    } catch (err) {
      // Log the error but do NOT log the app password or key (T-03-04)
      console.error(
        `[broker/poller] Error processing credential id=${cred.id} (${cred.fastmailEmail}):`,
        err instanceof Error ? err.message : String(err),
      );
    }
  }
}

/**
 * Starts the 5-minute background polling schedule.
 * Call once at API startup (Plan 04 wires this into index.ts).
 * Uses setInterval instead of node-cron: node-cron 4.2.1 silently skipped executions
 * in the long-running server process; setInterval fires reliably.
 */
export function startBrokerPoller(): void {
  setInterval(
    () => {
      runPoll().catch((err: unknown) => {
        console.error('[broker/poller] Unhandled runPoll error:', err);
      });
    },
    5 * 60 * 1000,
  );
}
