import { db } from "@/lib/db";
import { encrypt, decrypt } from "@/lib/crypto";
import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api";

export class OAuthTokenError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "OAuthTokenError";
  }
}

export async function markConnectionTokenError(
  connectionId: string,
  message: string
): Promise<void> {
  await db.serviceConnection.update({
    where: { id: connectionId },
    data: { status: "error", lastError: message },
  });
}

// Marks a connection as permanently revoked and notifies all project team members.
// Safe to call on an already-revoked connection — skips both the DB write and notifications.
export async function revokeConnectionWithNotification(
  connectionId: string,
  message: string
): Promise<void> {
  const conn = await db.serviceConnection.findUnique({
    where: { id: connectionId },
    select: {
      id: true,
      status: true,
      provider: true,
      accountEmail: true,
      projectId: true,
    },
  });
  if (!conn || conn.status === "revoked") return;

  await db.serviceConnection.update({
    where: { id: connectionId },
    data: { status: "revoked", lastError: message },
  });

  const teamMembers = await db.teamMember.findMany({
    where: { projectId: conn.projectId },
    select: { userId: true },
  });
  if (teamMembers.length > 0) {
    await db.notification.createMany({
      data: teamMembers.map((m) => ({
        userId: m.userId,
        type: "error",
        title: "Reconnect required",
        message: `Access to ${conn.provider} (${conn.accountEmail}) has been revoked. Please reconnect the account in project settings.`,
      })),
    });
  }
}

const STATIC_PROVIDERS = new Set([
  "github", "slack", "notion",
  // API key providers — tokens never expire, just decrypt and use
  "openRouter", "telegram", "semrush", "ahrefs", "stripe", "airtable", "calendly",
]);
const GOOGLE_PROVIDERS = new Set([
  "gmail",
  "calendar",
  "drive",
  "googleAds",
  "searchConsole",
  "youtube",
  "googleTagManager",
]);
// Providers that use the current access token (not a refresh_token) to obtain a new token
const EXCHANGE_PROVIDERS = new Set(["threads", "metaAds"]);
const tracer = trace.getTracer("scopegate/oauth-token-lifecycle");

type StandardTokenResponse = {
  access_token: string;
  expires_in: number;
  refresh_token?: string;
};

async function exchangeMetaToken(
  currentToken: string,
  appId: string,
  appSecret: string
): Promise<StandardTokenResponse> {
  return tracer.startActiveSpan(
    "GET graph.facebook.com",
    {
      kind: SpanKind.CLIENT,
      attributes: {
        "http.method": "GET",
        "mcp.provider": "metaAds",
        "peer.service": "graph.facebook.com",
        "url.path": "/v21.0/oauth/access_token",
      },
    },
    async (span) => {
      try {
        const params = new URLSearchParams({
          grant_type: "fb_exchange_token",
          client_id: appId,
          client_secret: appSecret,
          fb_exchange_token: currentToken,
        });
        const res = await fetch(
          `https://graph.facebook.com/v21.0/oauth/access_token?${params}`
        );
        span.setAttribute("http.status_code", res.status);

        if (!res.ok) {
          let code: number | undefined;
          let detail = "";
          try {
            const body = (await res.json()) as {
              error?: { code?: number; message?: string };
            };
            code = body.error?.code;
            detail = body.error?.message ?? "";
          } catch {
            // non-JSON error body — fall through with status only
          }

          if (code != null) {
            span.setAttribute("error.code", code);
            span.setAttribute("error.type", String(code));
          } else {
            span.setAttribute("error.type", String(res.status));
          }
          if (detail) span.setAttribute("error.message", detail.slice(0, 512));
          span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${res.status}` });

          throw new OAuthTokenError(
            `Meta token exchange failed (${res.status})` +
              (code != null ? ` code=${code}` : "") +
              (detail ? `: ${detail}` : "")
          );
        }

        return res.json() as Promise<StandardTokenResponse>;
      } catch (err) {
        if (!(err instanceof OAuthTokenError)) {
          const message = err instanceof Error ? err.message : "Unknown Meta token exchange error";
          span.setStatus({ code: SpanStatusCode.ERROR, message });
          span.recordException(err instanceof Error ? err : new Error(message));
        }
        throw err;
      } finally {
        span.end();
      }
    }
  );
}

type ProviderConfig =
  | { kind: "static" }
  | {
      kind: "refresh";
      bufferMs: number;
      doRefresh: (refreshToken: string) => Promise<StandardTokenResponse>;
    }
  | {
      kind: "exchange";
      bufferMs: number;
      // Throws OAuthTokenError on failure. The cron path (refreshForCron) lets the
      // throw drive the circuit breaker (status/backoff); the on-demand path
      // (getValidAccessTokenForConnection) catches it and falls back to the current token.
      doExchange: (currentToken: string) => Promise<StandardTokenResponse>;
    };

function getProviderConfig(provider: string): ProviderConfig {
  if (STATIC_PROVIDERS.has(provider)) return { kind: "static" };

  if (GOOGLE_PROVIDERS.has(provider)) {
    const clientId = process.env.GOOGLE_CLIENT_ID!;
    const clientSecret = process.env.GOOGLE_CLIENT_SECRET!;
    return {
      kind: "refresh",
      bufferMs: 5 * 60 * 1000,
      doRefresh: async (refreshToken) => {
        const res = await fetch("https://oauth2.googleapis.com/token", {
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            refresh_token: refreshToken,
            client_id: clientId,
            client_secret: clientSecret,
            grant_type: "refresh_token",
          }),
        });
        if (!res.ok) throw new OAuthTokenError(`Google token refresh failed (${res.status})`);
        return res.json() as Promise<StandardTokenResponse>;
      },
    };
  }

  if (provider === "hubspot") {
    const clientId = process.env.HUBSPOT_CLIENT_ID!;
    const clientSecret = process.env.HUBSPOT_CLIENT_SECRET!;
    return {
      kind: "refresh",
      bufferMs: 5 * 60 * 1000,
      doRefresh: async (refreshToken) => {
        const res = await fetch("https://api.hubapi.com/oauth/v1/token", {
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            grant_type: "refresh_token",
            refresh_token: refreshToken,
            client_id: clientId,
            client_secret: clientSecret,
          }),
        });
        if (!res.ok) throw new OAuthTokenError(`HubSpot token refresh failed (${res.status})`);
        return res.json() as Promise<StandardTokenResponse>;
      },
    };
  }

  if (provider === "jira") {
    const clientId = process.env.JIRA_CLIENT_ID!;
    const clientSecret = process.env.JIRA_CLIENT_SECRET!;
    return {
      kind: "refresh",
      bufferMs: 5 * 60 * 1000,
      doRefresh: async (refreshToken) => {
        const res = await fetch("https://auth.atlassian.com/oauth/token", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            grant_type: "refresh_token",
            client_id: clientId,
            client_secret: clientSecret,
            refresh_token: refreshToken,
          }),
        });
        if (!res.ok) throw new OAuthTokenError(`Jira token refresh failed (${res.status})`);
        return res.json() as Promise<StandardTokenResponse>;
      },
    };
  }

  if (provider === "linkedin") {
    const clientId = process.env.LINKEDIN_CLIENT_ID!;
    const clientSecret = process.env.LINKEDIN_CLIENT_SECRET!;
    return {
      kind: "refresh",
      bufferMs: 5 * 60 * 1000,
      doRefresh: async (refreshToken) => {
        const res = await fetch("https://www.linkedin.com/oauth/v2/accessToken", {
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            grant_type: "refresh_token",
            refresh_token: refreshToken,
            client_id: clientId,
            client_secret: clientSecret,
          }),
        });
        if (!res.ok) throw new OAuthTokenError(`LinkedIn token refresh failed (${res.status})`);
        return res.json() as Promise<StandardTokenResponse>;
      },
    };
  }

  if (provider === "salesforce") {
    const clientId = process.env.SALESFORCE_CLIENT_ID!;
    const clientSecret = process.env.SALESFORCE_CLIENT_SECRET!;
    return {
      kind: "refresh",
      bufferMs: 5 * 60 * 1000,
      doRefresh: async (refreshToken) => {
        const res = await fetch("https://login.salesforce.com/services/oauth2/token", {
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            grant_type: "refresh_token",
            refresh_token: refreshToken,
            client_id: clientId,
            client_secret: clientSecret,
          }),
        });
        if (!res.ok) throw new OAuthTokenError(`Salesforce token refresh failed (${res.status})`);
        const data = (await res.json()) as { access_token: string };
        // Salesforce doesn't return expires_in — assume 2 hours
        return { access_token: data.access_token, expires_in: 2 * 60 * 60 };
      },
    };
  }

  if (provider === "twitter") {
    const clientId = process.env.TWITTER_CLIENT_ID!;
    const clientSecret = process.env.TWITTER_CLIENT_SECRET!;
    return {
      kind: "refresh",
      bufferMs: 5 * 60 * 1000,
      doRefresh: async (refreshToken) => {
        const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
        const res = await fetch("https://api.x.com/2/oauth2/token", {
          method: "POST",
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            Authorization: `Basic ${basicAuth}`,
          },
          body: new URLSearchParams({
            grant_type: "refresh_token",
            refresh_token: refreshToken,
          }),
        });
        if (!res.ok) throw new OAuthTokenError(`Twitter token refresh failed (${res.status})`);
        return res.json() as Promise<StandardTokenResponse>;
      },
    };
  }

  if (provider === "metaAds") {
    const appId = process.env.META_APP_ID!;
    const appSecret = process.env.META_APP_SECRET!;
    return {
      kind: "exchange",
      bufferMs: 24 * 60 * 60 * 1000,
      doExchange: async (currentToken) => {
        return exchangeMetaToken(currentToken, appId, appSecret);
      },
    };
  }

  if (provider === "threads") {
    return {
      kind: "exchange",
      bufferMs: 24 * 60 * 60 * 1000,
      doExchange: async (currentToken) => {
        const params = new URLSearchParams({
          grant_type: "th_refresh_token",
          access_token: currentToken,
        });
        const res = await fetch(
          `https://graph.threads.net/refresh_access_token?${params}`
        );
        if (!res.ok) throw new OAuthTokenError(`Threads token refresh failed (${res.status})`);
        return res.json() as Promise<StandardTokenResponse>;
      },
    };
  }

  throw new Error(`Unknown OAuth provider: ${provider}`);
}

type DbConnection = Awaited<ReturnType<typeof db.serviceConnection.findUniqueOrThrow>>;

export async function getValidAccessToken(connectionId: string): Promise<string> {
  const conn = await db.serviceConnection.findUniqueOrThrow({
    where: { id: connectionId },
  });
  return getValidAccessTokenForConnection(conn);
}

export async function getValidAccessTokenForConnection(conn: DbConnection): Promise<string> {
  if (conn.status === "revoked") {
    throw new OAuthTokenError(
      `Connection ${conn.id} (${conn.provider}) is revoked — reconnect required`
    );
  }

  const config = getProviderConfig(conn.provider);

  if (config.kind === "static") {
    return decrypt(conn.accessToken);
  }

  const needsRefresh =
    !conn.expiresAt || conn.expiresAt.getTime() < Date.now() + config.bufferMs;

  if (!needsRefresh) {
    return decrypt(conn.accessToken);
  }

  if (config.kind === "exchange") {
    const currentToken = decrypt(conn.accessToken);
    let result: StandardTokenResponse;
    try {
      result = await config.doExchange(currentToken);
    } catch {
      // On-demand resilience: fall back to the current token if the exchange
      // fails. The cron path (refreshForCron) owns status/backoff via the breaker.
      return currentToken;
    }
    await db.serviceConnection.update({
      where: { id: conn.id },
      data: {
        accessToken: encrypt(result.access_token),
        expiresAt: new Date(Date.now() + result.expires_in * 1000),
        status: "active",
        lastError: null,
      },
    });
    return result.access_token;
  }

  // kind === "refresh"
  if (!conn.refreshToken) {
    throw new OAuthTokenError(
      `No refresh token available for connection ${conn.id} (${conn.provider})`
    );
  }
  const tokens = await config.doRefresh(decrypt(conn.refreshToken));
  const updateData: Record<string, unknown> = {
    accessToken: encrypt(tokens.access_token),
    expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
    status: "active",
    lastError: null,
  };
  if (tokens.refresh_token) {
    updateData.refreshToken = encrypt(tokens.refresh_token);
  }
  await db.serviceConnection.update({ where: { id: conn.id }, data: updateData });
  return tokens.access_token;
}

export type RefreshForCronOutcome = "refreshed" | "skipped";

export type CronConnection = {
  id: string;
  provider: string;
  accessToken: string;
  refreshToken: string | null;
  expiresAt: Date | null;
};

// Performs a proactive token refresh for cron use. Resets consecutiveFailures on success.
// Returns "skipped" for static tokens (nothing to refresh).
// Throws on refresh/exchange failure — callers apply error state + backoff (circuit breaker).
export async function refreshForCron(
  connection: CronConnection
): Promise<RefreshForCronOutcome> {
  const config = getProviderConfig(connection.provider);

  if (config.kind === "static") return "skipped";

  if (config.kind === "exchange") {
    const currentToken = decrypt(connection.accessToken);
    const result = await config.doExchange(currentToken);
    await db.serviceConnection.update({
      where: { id: connection.id },
      data: {
        accessToken: encrypt(result.access_token),
        expiresAt: new Date(Date.now() + result.expires_in * 1000),
        status: "active",
        lastError: null,
        consecutiveFailures: 0,
      },
    });
    return "refreshed";
  }

  // kind === "refresh"
  if (!connection.refreshToken) return "skipped";

  const tokens = await config.doRefresh(decrypt(connection.refreshToken));
  const updateData: Record<string, unknown> = {
    accessToken: encrypt(tokens.access_token),
    expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
    status: "active",
    lastError: null,
    consecutiveFailures: 0,
  };
  if (tokens.refresh_token) {
    updateData.refreshToken = encrypt(tokens.refresh_token);
  }
  await db.serviceConnection.update({ where: { id: connection.id }, data: updateData });
  return "refreshed";
}

export { EXCHANGE_PROVIDERS };
