import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { refreshAccessToken } from "./auth.js";

export type OutlookCredentials = {
  access: string;
  refresh: string;
  expires: number;
  email?: string;
  displayName?: string;
  updatedAt?: string;
  /**
   * Unix ms of the most recent moment we KNOW the refresh token worked —
   * set on OAuth-flow success and on every successful refreshAccessToken.
   * Consumed by `resolveConnectionState` to tell the difference between
   * "we hold a refresh string" (previous weak check) and "the refresh
   * string actually produced a valid access token recently." If this
   * field is absent on an existing credentials file (pre-2.8.11), we
   * treat it as "never verified" and the state falls back to "stale" —
   * a safe default that surfaces the uncertainty rather than silently
   * claiming "connected."
   */
  lastVerifiedAt?: number;
};

/**
 * How long a successful refresh is trusted before we downgrade the
 * reported connection state to "stale" (still usable — the next tool
 * call will attempt a refresh — but the system prompt hedges the claim
 * so the agent doesn't assert a connection that may have been
 * invalidated upstream).
 */
export const REFRESH_TRUST_WINDOW_MS = 24 * 60 * 60 * 1000;

/**
 * Tri-state connection classification. Consumed by the plugin's system-
 * prompt injection (extensions/outlook/index.ts) and by the gateway
 * status RPC (src/gateway/server-methods/channels.ts) so both surface
 * the same truth.
 *
 * - valid         access token still inside its expiry window
 * - trusted       access expired, but refresh was verified < 24h ago
 * - stale         access expired, refresh never verified or > 24h ago
 *                 (still attempt refresh on next tool call; may or may
 *                 not succeed — Microsoft could have invalidated the
 *                 refresh token without telling us)
 * - not-connected no credentials on disk, or creds file is unreadable
 */
export type OutlookConnectionState = "valid" | "trusted" | "stale" | "not-connected";

function resolveHome(): string {
  const home = os.homedir();
  if (home && home.length > 0) {
    return home;
  }
  // Defence-in-depth — os.homedir() is the canonical resolution and
  // consults /etc/passwd / platform APIs before falling back to the env.
  // If it returns empty, environment variables are the next-best bet.
  // Absolute last resort is /var/symi rather than /tmp — /tmp is purged
  // by the OS on a cadence that silently eats OAuth tokens.
  return process.env.HOME || process.env.USERPROFILE || "/var/symi";
}

export function getStoreDir(): string {
  return path.join(resolveHome(), ".symi", "credentials");
}

export function getStorePath(): string {
  return path.join(getStoreDir(), "outlook.json");
}

export function loadCredentials(): OutlookCredentials | null {
  try {
    const data = fs.readFileSync(getStorePath(), "utf-8");
    return JSON.parse(data) as OutlookCredentials;
  } catch {
    return null;
  }
}

/**
 * Atomic write: tmp file + fsync + rename. Prevents torn writes if the
 * process crashes mid-save (which would otherwise leave a zero-byte
 * file and look like a missing credential to every subsequent load).
 *
 * Caller contract: every save is VERIFIED — saveCredentials only runs
 * after a successful OAuth flow or successful refreshAccessToken, so
 * stamping `lastVerifiedAt = now` unconditionally is correct. If a
 * future code path needs to save unverified credentials (e.g. partial
 * OAuth in progress), introduce a separate saveUnverifiedCredentials
 * that skips the stamp.
 */
export function saveCredentials(creds: OutlookCredentials): void {
  const dir = getStoreDir();
  fs.mkdirSync(dir, { recursive: true });
  const now = Date.now();
  const stamped: OutlookCredentials = {
    ...creds,
    updatedAt: new Date(now).toISOString(),
    lastVerifiedAt: now,
  };
  const finalPath = getStorePath();
  const tmpPath = `${finalPath}.tmp-${process.pid}-${now}`;
  fs.writeFileSync(tmpPath, JSON.stringify(stamped, null, 2), { mode: 0o600 });
  fs.renameSync(tmpPath, finalPath);
}

export function deleteCredentials(): boolean {
  try {
    fs.unlinkSync(getStorePath());
    return true;
  } catch {
    return false;
  }
}

/**
 * Returns a valid access token, refreshing if expired.
 * Throws if no credentials stored or refresh fails.
 *
 * On successful refresh, re-saves credentials — which re-stamps
 * `lastVerifiedAt` via the saveCredentials contract. This is how
 * `stale` transitions back to `trusted` (and the agent's system-
 * prompt block flips back to the unqualified "Connected as X" form)
 * as soon as a tool call succeeds.
 */
export async function getAccessToken(): Promise<string> {
  const creds = loadCredentials();
  if (!creds) {
    throw new Error(
      "Outlook not connected. Run `symi outlook login` to sign in with your Microsoft account.",
    );
  }

  // Token still valid
  if (Date.now() < creds.expires) {
    return creds.access;
  }

  // Refresh the token
  const refreshed = await refreshAccessToken(creds.refresh);
  saveCredentials({
    ...creds,
    access: refreshed.access,
    refresh: refreshed.refresh,
    expires: refreshed.expires,
  });
  return refreshed.access;
}

/**
 * Classify the current connection based on the credential state — used
 * by both the plugin's `before_prompt_build` hook and the gateway's
 * `channels.status` RPC so both surfaces agree on the truth.
 *
 * See the OutlookConnectionState doc for the meaning of each state.
 */
export function resolveConnectionState(
  creds: OutlookCredentials | null,
  now: number = Date.now(),
): OutlookConnectionState {
  if (!creds) {
    return "not-connected";
  }
  if (typeof creds.expires === "number" && now < creds.expires) {
    return "valid";
  }
  if (
    typeof creds.lastVerifiedAt === "number" &&
    now - creds.lastVerifiedAt < REFRESH_TRUST_WINDOW_MS
  ) {
    return "trusted";
  }
  if (typeof creds.refresh === "string" && creds.refresh.length > 0) {
    return "stale";
  }
  return "not-connected";
}
