/**
 * Threads (Meta Threads) Auth Service
 *
 * Threads OAuth 2.0 flow for connecting Threads publish accounts (Social Media
 * OS). One connect yields a short-lived token which we immediately exchange for
 * a LONG-LIVED token (~60 days) and store a single ThreadsAccount for the user.
 *
 * Each connection is scoped to (companyId, userId, threadsUserId) — every admin
 * connects their own account; tokens are encrypted at rest (AES-256-GCM). The
 * OAuth `state` is a stateless HMAC-signed payload that binds the callback to
 * the initiating admin.
 *
 * Platform credentials come from ThreadsAppConfig (Super Admin), env fallback.
 *
 * Threads specifics:
 *  - Its OWN OAuth/Graph endpoints (threads.net / graph.threads.net) and its own
 *    App ID/Secret — NOT the shared Meta/Facebook app the Instagram integration
 *    reuses.
 *  - Scopes are COMMA-separated in the authorize URL. No PKCE.
 *  - Long-lived tokens are refreshed IN PLACE via `th_refresh_token` (the token
 *    must be >=24h old); there is no separate rotating refresh token.
 */

import crypto from 'crypto';
import { encryptApiKey, decryptApiKey } from '../utils/encryption';
import type { IThreadsAccount } from '../../models/ThreadsAccount';

// ============================================
// CONFIGURATION
// ============================================

const ENV_CLIENT_ID = process.env.THREADS_APP_ID || process.env.THREADS_CLIENT_ID || '';
const ENV_CLIENT_SECRET = process.env.THREADS_APP_SECRET || process.env.THREADS_CLIENT_SECRET || '';
const DEFAULT_REDIRECT_URL = process.env.THREADS_REDIRECT_URI || process.env.THREADS_REDIRECT_URL || 'http://localhost:3101/api/threads/auth/callback';
const DEFAULT_GRAPH_VERSION = process.env.THREADS_GRAPH_VERSION || 'v1.0';
const DEFAULT_SCOPE = process.env.THREADS_SCOPES || 'threads_basic,threads_content_publish';

const OAUTH_AUTH_URL = 'https://threads.net/oauth/authorize';
const OAUTH_TOKEN_URL = 'https://graph.threads.net/oauth/access_token';
const GRAPH_BASE = 'https://graph.threads.net';

const STATE_SECRET: string = process.env.JWT_SECRET || 'dev-only-insecure-jwt-secret-do-not-use-in-production';
const STATE_TTL_MS = 10 * 60 * 1000; // 10 minutes
// Refresh the long-lived token when it has less than this much life left.
const REFRESH_HEADROOM_MS = 7 * 24 * 3600 * 1000; // 7 days
const LONG_LIVED_DEFAULT_SECONDS = 60 * 24 * 3600; // ~60 days

const NOT_CONFIGURED_ERROR =
  'Threads publishing is not configured for this platform yet. A super admin must add the Threads app credentials in Super Admin → Settings.';

const PLATFORM_CONFIG_KEY = 'platform';

// ============================================
// CREDENTIAL RESOLUTION (super-admin saved config first, env fallback)
// ============================================

export interface ThreadsCredentials {
  clientId: string;
  clientSecret: string;
  redirectUrl: string;
  graphVersion: string;
  scope: string;
  source: 'platform' | 'env';
}

export async function getThreadsCredentials(): Promise<ThreadsCredentials | null> {
  try {
    const { getModels } = await import('../../models');
    const { ThreadsAppConfig } = getModels();

    const config = await ThreadsAppConfig.findOne({ companyId: PLATFORM_CONFIG_KEY })
      .select('+encryptedClientId +clientIdIV +encryptedClientSecret +clientSecretIV')
      || await ThreadsAppConfig.findOne({})
        .sort({ updatedAt: -1 })
        .select('+encryptedClientId +clientIdIV +encryptedClientSecret +clientSecretIV');

    if (config?.encryptedClientId && config?.clientIdIV && config?.encryptedClientSecret && config?.clientSecretIV) {
      return {
        clientId: decryptApiKey(config.encryptedClientId, config.clientIdIV),
        clientSecret: decryptApiKey(config.encryptedClientSecret, config.clientSecretIV),
        redirectUrl: config.redirectUrl || DEFAULT_REDIRECT_URL,
        graphVersion: config.graphVersion || DEFAULT_GRAPH_VERSION,
        scope: config.scope || DEFAULT_SCOPE,
        source: 'platform',
      };
    }
  } catch (error) {
    console.error('Failed to load platform Threads credentials:', error);
  }

  if (ENV_CLIENT_ID && ENV_CLIENT_SECRET) {
    return {
      clientId: ENV_CLIENT_ID,
      clientSecret: ENV_CLIENT_SECRET,
      redirectUrl: DEFAULT_REDIRECT_URL,
      graphVersion: DEFAULT_GRAPH_VERSION,
      scope: DEFAULT_SCOPE,
      source: 'env',
    };
  }

  return null;
}

export async function saveThreadsCredentials(
  userId: string,
  input: { clientId: string; clientSecret: string; redirectUrl?: string; graphVersion?: string; scope?: string }
): Promise<{ success: boolean; error?: string }> {
  const clientId = (input.clientId || '').trim();
  const clientSecret = (input.clientSecret || '').trim();
  const redirectUrl = (input.redirectUrl || '').trim() || DEFAULT_REDIRECT_URL;
  const graphVersion = (input.graphVersion || '').trim() || DEFAULT_GRAPH_VERSION;
  const scope = (input.scope || '').trim() || DEFAULT_SCOPE;

  if (!clientId) {
    return { success: false, error: 'App ID is required' };
  }
  if (!clientSecret) {
    return { success: false, error: 'App Secret is required' };
  }
  try {
    new URL(redirectUrl);
  } catch {
    return { success: false, error: 'Redirect URL must be a valid URL' };
  }

  const { getModels } = await import('../../models');
  const { ThreadsAppConfig } = getModels();

  const encryptedId = encryptApiKey(clientId);
  const encryptedSecret = encryptApiKey(clientSecret);

  await ThreadsAppConfig.deleteMany({});
  await ThreadsAppConfig.create({
    companyId: PLATFORM_CONFIG_KEY,
    encryptedClientId: encryptedId.encrypted,
    clientIdIV: encryptedId.iv,
    encryptedClientSecret: encryptedSecret.encrypted,
    clientSecretIV: encryptedSecret.iv,
    redirectUrl,
    graphVersion,
    scope,
    updatedBy: userId,
  });

  return { success: true };
}

export async function deleteThreadsCredentials(): Promise<void> {
  const { getModels } = await import('../../models');
  const { ThreadsAppConfig } = getModels();
  await ThreadsAppConfig.deleteMany({});
}

export async function getCredentialStatus(): Promise<{
  configured: boolean;
  source: 'platform' | 'env' | null;
  clientIdMasked?: string;
  redirectUrl: string;
  graphVersion: string;
  scope: string;
}> {
  const credentials = await getThreadsCredentials();
  if (!credentials) {
    return { configured: false, source: null, redirectUrl: DEFAULT_REDIRECT_URL, graphVersion: DEFAULT_GRAPH_VERSION, scope: DEFAULT_SCOPE };
  }
  return {
    configured: true,
    source: credentials.source,
    clientIdMasked: `${credentials.clientId.slice(0, 4)}…${credentials.clientId.slice(-4)}`,
    redirectUrl: credentials.redirectUrl,
    graphVersion: credentials.graphVersion,
    scope: credentials.scope,
  };
}

// ============================================
// NETWORK: resilient fetch
// ============================================

async function thFetch(url: string, init: RequestInit = {}, attempts = 3, timeoutMs = 20000): Promise<Response> {
  let lastError: unknown;
  for (let attempt = 1; attempt <= attempts; attempt++) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      return await fetch(url, { ...init, signal: controller.signal });
    } catch (error) {
      lastError = error;
      if (attempt < attempts) {
        await new Promise((resolve) => setTimeout(resolve, attempt * 1000 - 500));
      }
    } finally {
      clearTimeout(timer);
    }
  }
  throw lastError;
}

// ============================================
// SIGNED STATE (stateless CSRF protection + admin binding)
// ============================================

interface OAuthStatePayload {
  companyId: string;
  userId: string;
  nonce: string;
  exp: number;
}

function signState(payload: OAuthStatePayload): string {
  const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
  const signature = crypto.createHmac('sha256', STATE_SECRET).update(body).digest('base64url');
  return `${body}.${signature}`;
}

export function verifyState(state: string): OAuthStatePayload | null {
  const [body, signature] = state.split('.');
  if (!body || !signature) return null;

  const expected = crypto.createHmac('sha256', STATE_SECRET).update(body).digest('base64url');
  const sigBuf = Buffer.from(signature);
  const expBuf = Buffer.from(expected);
  if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) return null;

  try {
    const payload = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as OAuthStatePayload;
    if (!payload.companyId || !payload.userId || !payload.exp) return null;
    if (payload.exp < Date.now()) return null;
    return payload;
  } catch {
    return null;
  }
}

function buildEncryptedTokenFields(accessToken: string) {
  const access = encryptApiKey(accessToken);
  return {
    encryptedAccessToken: access.encrypted,
    accessTokenIV: access.iv,
  };
}

function extractThError(body: string): string | null {
  try {
    const parsed = JSON.parse(body);
    return parsed?.error_message || parsed?.error?.message || parsed?.error_description || parsed?.error || null;
  } catch {
    return null;
  }
}

// ============================================
// OAUTH FLOW
// ============================================

export async function getAuthUrl(companyId: string, userId: string): Promise<{ url: string; state: string; error?: string }> {
  const credentials = await getThreadsCredentials();
  if (!credentials) {
    return { url: '', state: '', error: NOT_CONFIGURED_ERROR };
  }

  const state = signState({
    companyId,
    userId,
    nonce: crypto.randomBytes(16).toString('hex'),
    exp: Date.now() + STATE_TTL_MS,
  });

  // Threads scopes are comma-separated in the authorize URL.
  const params = new URLSearchParams({
    client_id: credentials.clientId,
    redirect_uri: credentials.redirectUrl,
    scope: credentials.scope,
    response_type: 'code',
    state,
  });

  return { url: `${OAUTH_AUTH_URL}?${params.toString()}`, state };
}

/**
 * Handle the OAuth callback — exchange the code for a short-lived token,
 * immediately upgrade it to a long-lived token, read the user profile, and
 * store a ThreadsAccount under the admin bound in the signed state.
 */
export async function handleCallback(code: string, state: string): Promise<{ success: boolean; username?: string; error?: string }> {
  const stateData = verifyState(state);
  if (!stateData) {
    return { success: false, error: 'Invalid or expired OAuth state' };
  }

  const { companyId, userId } = stateData;

  const credentials = await getThreadsCredentials();
  if (!credentials) {
    return { success: false, error: NOT_CONFIGURED_ERROR };
  }

  const { clientId, clientSecret, redirectUrl, graphVersion } = credentials;

  try {
    // 1. Exchange the code for a SHORT-LIVED token + user_id
    const tokenResponse = await thFetch(OAUTH_TOKEN_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        client_id: clientId,
        client_secret: clientSecret,
        grant_type: 'authorization_code',
        redirect_uri: redirectUrl,
        code,
      }).toString(),
    });

    if (!tokenResponse.ok) {
      const body = await tokenResponse.text();
      console.error('Threads token exchange failed with status', tokenResponse.status);
      return { success: false, error: extractThError(body) || 'Failed to exchange authorization code' };
    }

    const shortTokens: any = await tokenResponse.json();
    const shortToken: string = shortTokens.access_token;
    const threadsUserId: string = String(shortTokens.user_id || '');
    if (!shortToken) {
      return { success: false, error: 'Threads did not return an access token' };
    }

    // 2. Upgrade to a LONG-LIVED token (~60 days)
    const longUrl = `${GRAPH_BASE}/access_token?grant_type=th_exchange_token&client_secret=${encodeURIComponent(clientSecret)}&access_token=${encodeURIComponent(shortToken)}`;
    const longResponse = await thFetch(longUrl);
    if (!longResponse.ok) {
      const body = await longResponse.text();
      return { success: false, error: extractThError(body) || 'Failed to obtain a long-lived Threads token' };
    }
    const longTokens: any = await longResponse.json();
    const accessToken: string = longTokens.access_token || shortToken;
    const expiresIn: number = longTokens.expires_in || LONG_LIVED_DEFAULT_SECONDS;

    // 3. User profile
    const meUrl = `${GRAPH_BASE}/${graphVersion}/me?fields=id,username,threads_profile_picture_url,threads_biography&access_token=${encodeURIComponent(accessToken)}`;
    const meResponse = await thFetch(meUrl);
    if (!meResponse.ok) {
      const body = await meResponse.text();
      return { success: false, error: extractThError(body) || 'Failed to read your Threads profile' };
    }
    const me: any = await meResponse.json();
    const resolvedUserId = String(me.id || threadsUserId || '');
    if (!resolvedUserId) {
      return { success: false, error: 'Threads did not return your user id' };
    }

    const { getModels } = await import('../../models');
    const { ThreadsAccount } = getModels();

    await ThreadsAccount.findOneAndUpdate(
      { companyId, userId, threadsUserId: resolvedUserId },
      {
        companyId,
        userId,
        accountType: 'user',
        threadsUserId: resolvedUserId,
        username: me.username || '',
        displayName: me.username || 'Threads user',
        profilePicture: me.threads_profile_picture_url || '',
        biography: me.threads_biography || '',
        ...buildEncryptedTokenFields(accessToken),
        tokenExpiresAt: new Date(Date.now() + expiresIn * 1000),
        scope: credentials.scope,
        status: 'connected',
        isDemo: false,
        connectedAt: new Date(),
      },
      { upsert: true, new: true }
    );

    return { success: true, username: me.username ? `@${me.username}` : 'your account' };
  } catch (error: any) {
    console.error('Threads OAuth callback error:', error);
    const cause = error?.cause?.code || error?.code || '';
    if (error?.name === 'AbortError' || String(cause).includes('TIMEOUT') || String(error?.message).includes('fetch failed')) {
      return { success: false, error: 'Could not reach Threads to complete the connection (network timeout). Check the server\'s internet connection or proxy/firewall, then try again.' };
    }
    return { success: false, error: 'OAuth callback failed' };
  }
}

// ============================================
// ACCOUNT MANAGEMENT (always scoped to the owning admin)
// ============================================

export async function listAccounts(companyId: string, userId: string) {
  const { getModels } = await import('../../models');
  const { ThreadsAccount } = getModels();

  const accounts = await ThreadsAccount.find({ companyId, userId }).sort({ connectedAt: -1 });

  return accounts.map((account: IThreadsAccount) => ({
    id: account._id,
    accountType: account.accountType,
    threadsUserId: account.threadsUserId,
    username: account.username,
    displayName: account.displayName,
    profilePicture: account.profilePicture,
    biography: account.biography,
    status: account.status,
    isDemo: account.isDemo,
    connectedAt: account.connectedAt,
    lastUsedAt: account.lastUsedAt,
  }));
}

/**
 * Disconnect an account — owner only. Deletes the record and cancels its pending
 * publications. (Threads has no user-token revocation endpoint; the token simply
 * expires.)
 */
export async function disconnect(companyId: string, userId: string, accountId: string): Promise<{ success: boolean; error?: string }> {
  try {
    const { getModels } = await import('../../models');
    const { ThreadsAccount } = getModels();

    const account = await ThreadsAccount.findOne({ _id: accountId, companyId, userId });
    if (!account) {
      return { success: false, error: 'Threads connection not found' };
    }

    await ThreadsAccount.findByIdAndDelete(account._id);

    const { SocialMediaPublication } = getModels();
    await SocialMediaPublication.updateMany(
      { accountRef: account._id.toString(), status: { $in: ['draft', 'queued', 'uploading', 'processing', 'scheduled'] } },
      {
        status: 'cancelled',
        workerLockedAt: null,
        lastError: {
          code: 'account_disconnected',
          message: 'The connected Threads account was disconnected before this publication completed',
          at: new Date(),
        },
      }
    );

    return { success: true };
  } catch (error) {
    console.error('Threads disconnect error:', error);
    return { success: false, error: 'Failed to disconnect Threads account' };
  }
}

// ============================================
// TOKEN ACCESS (for the publishing service)
// ============================================

/**
 * Get a fresh long-lived access token for an account owned by the given admin.
 * Threads long-lived tokens last ~60 days and are refreshed IN PLACE via
 * `th_refresh_token` (the token must be >=24h old). We refresh proactively when
 * within REFRESH_HEADROOM_MS of expiry. This is the publishing isolation
 * checkpoint — the account is loaded by (id, companyId, userId).
 */
export async function getFreshToken(companyId: string, userId: string, accountId: string): Promise<{ accessToken?: string; account?: IThreadsAccount; error?: string }> {
  const { getModels } = await import('../../models');
  const { ThreadsAccount } = getModels();

  const account = await ThreadsAccount.findOne({ _id: accountId, companyId, userId })
    .select('+encryptedAccessToken +accessTokenIV');

  if (!account) {
    return { error: 'Threads connection not found' };
  }
  if (account.status === 'revoked') {
    return { error: 'This Threads connection has been revoked' };
  }
  if (account.isDemo) {
    return { error: 'This account was connected in demo mode. Disconnect it and reconnect the real account.' };
  }
  if (!account.encryptedAccessToken || !account.accessTokenIV) {
    return { error: 'Threads connection is missing credentials. Please reconnect.' };
  }

  const currentToken = decryptApiKey(account.encryptedAccessToken, account.accessTokenIV);

  // Still fresh (more than the refresh headroom left)
  if (account.tokenExpiresAt && new Date(account.tokenExpiresAt).getTime() > Date.now() + REFRESH_HEADROOM_MS) {
    account.lastUsedAt = new Date();
    await account.save();
    return { accessToken: currentToken, account };
  }

  // Refresh the long-lived token in place.
  try {
    const refreshUrl = `${GRAPH_BASE}/refresh_access_token?grant_type=th_refresh_token&access_token=${encodeURIComponent(currentToken)}`;
    const response = await thFetch(refreshUrl);

    if (!response.ok) {
      // If it is still valid, keep using it; otherwise force a reconnect.
      if (account.tokenExpiresAt && new Date(account.tokenExpiresAt).getTime() > Date.now()) {
        account.lastUsedAt = new Date();
        await account.save();
        return { accessToken: currentToken, account };
      }
      account.status = 'reconnect_required';
      await account.save();
      return { error: 'Your Threads access has expired. Please reconnect.' };
    }

    const tokens: any = await response.json();
    const newToken: string = tokens.access_token || currentToken;
    const expiresIn: number = tokens.expires_in || LONG_LIVED_DEFAULT_SECONDS;

    const encrypted = encryptApiKey(newToken);
    account.encryptedAccessToken = encrypted.encrypted;
    account.accessTokenIV = encrypted.iv;
    account.tokenExpiresAt = new Date(Date.now() + expiresIn * 1000);
    account.status = 'connected';
    account.lastUsedAt = new Date();
    await account.save();

    return { accessToken: newToken, account };
  } catch (error) {
    console.error('Threads token refresh error:', error);
    // Fall back to the current token if it is not yet expired.
    if (account.tokenExpiresAt && new Date(account.tokenExpiresAt).getTime() > Date.now()) {
      return { accessToken: currentToken, account };
    }
    return { error: 'Failed to refresh Threads access token' };
  }
}
