import { db, schema } from './db';
import { eq } from 'drizzle-orm';

const FIC_BASE = 'https://api-v2.fattureincloud.it';

// ── Types ─────────────────────────────────────────────────────────────────────

export interface FicEntity {
  name: string;
  email?: string;
  vat_number?: string;
  tax_code?: string;
  address_street?: string;
  address_city?: string;
  address_postal_code?: string;
  address_country?: string;
}

export interface FicLineItem {
  name: string;
  qty: number;
  /**
   * Gross unit price in EUR (price the customer actually pays, VAT included).
   * We always use gross prices so the payment amount matches the invoice total
   * regardless of the VAT type id configured.
   */
  gross_price: number;
  /** FIC internal VAT type id */
  vat: { id: number };
}

export interface CreateInvoiceParams {
  /** ISO date string YYYY-MM-DD */
  date: string;
  entity: FicEntity;
  items: FicLineItem[];
  /** Total amount in cents (used for the payments_list entry) */
  amountCents: number;
  notes?: string;
}

interface FicConfig {
  clientId: string;
  clientSecret: string;
  companyId: string;
  vatId: number;
  accessToken: string | null;
  refreshToken: string | null;
  expiresAt: Date | null;
}

// ── Config / token management ─────────────────────────────────────────────────

/**
 * Load FIC config from DB. Falls back to env vars for bootstrapping —
 * after the first admin save the DB row is the source of truth.
 */
async function loadConfig(): Promise<FicConfig> {
  const row = await db.query.ficSettings.findFirst({
    where: eq(schema.ficSettings.id, 'default'),
  });

  if (row?.clientId && row.clientSecret && row.companyId) {
    return {
      clientId: row.clientId,
      clientSecret: row.clientSecret,
      companyId: row.companyId,
      vatId: parseInt(row.vatId ?? '0', 10),
      accessToken: row.accessToken ?? null,
      refreshToken: row.refreshToken ?? null,
      expiresAt: row.expiresAt ?? null,
    };
  }

  // Env-var fallback (bootstrap path)
  const clientId = process.env.FATTURE_IN_CLOUD_CLIENT_ID ?? '';
  const clientSecret = process.env.FATTURE_IN_CLOUD_CLIENT_SECRET ?? '';
  const companyId = process.env.FATTURE_IN_CLOUD_COMPANY_ID ?? '';

  if (!clientId || !clientSecret || !companyId) {
    throw new Error(
      'Fatture in Cloud is not configured. Set credentials in Admin → FIC Settings or via env vars.',
    );
  }

  const accessToken = process.env.FATTURE_IN_CLOUD_ACCESS_TOKEN ?? null;
  const refreshToken = process.env.FATTURE_IN_CLOUD_REFRESH_TOKEN ?? null;
  const vatId = parseInt(process.env.FATTURE_IN_CLOUD_VAT_ID ?? '0', 10);
  const expiresAt = accessToken ? new Date(Date.now() + 86_400_000) : null; // FIC access tokens last 24h

  // Persist bootstrap config so subsequent calls use DB
  await db.insert(schema.ficSettings)
    .values({
      id: 'default',
      clientId,
      clientSecret,
      companyId,
      vatId: String(vatId),
      accessToken,
      refreshToken,
      expiresAt,
    })
    .onConflictDoUpdate({
      target: schema.ficSettings.id,
      set: { clientId, clientSecret, companyId, vatId: String(vatId), updatedAt: new Date() },
    });

  return { clientId, clientSecret, companyId, vatId, accessToken, refreshToken, expiresAt };
}

/** Persist updated tokens to DB after a refresh. */
async function saveTokens(accessToken: string, refreshToken: string, expiresAt: Date): Promise<void> {
  await db.update(schema.ficSettings)
    .set({ accessToken, refreshToken, expiresAt, updatedAt: new Date() })
    .where(eq(schema.ficSettings.id, 'default'));
}

/** Exchange refresh token for a new access token. */
async function doRefresh(clientId: string, clientSecret: string, refreshToken: string): Promise<{
  accessToken: string;
  refreshToken: string;
  expiresAt: Date;
}> {
  const res = await fetch(`${FIC_BASE}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: clientId,
      client_secret: clientSecret,
      refresh_token: refreshToken,
    }),
  });

  if (!res.ok) {
    throw new Error(`FIC token refresh failed (${res.status}): ${await res.text()}`);
  }

  const data = await res.json() as { access_token: string; refresh_token: string; expires_in?: number };
  const expiresAt = new Date(Date.now() + (data.expires_in ?? 3_600) * 1_000);

  await saveTokens(data.access_token, data.refresh_token, expiresAt);

  return { accessToken: data.access_token, refreshToken: data.refresh_token, expiresAt };
}

/** Return a valid bearer token, refreshing proactively when within 5 minutes of expiry. */
async function getValidToken(): Promise<{ token: string; config: FicConfig }> {
  const config = await loadConfig();

  if (!config.accessToken) {
    throw new Error('No FIC access token. Configure one in Admin → FIC Settings.');
  }

  // Only proactively refresh when we both know the expiry AND have a refresh token.
  // Manual auth tokens (no refresh token) never expire — skip the check entirely.
  if (config.expiresAt !== null && config.refreshToken !== null) {
    const expiresInMs = config.expiresAt.getTime() - Date.now();
    if (expiresInMs < 5 * 60_000) {
      const refreshed = await doRefresh(config.clientId, config.clientSecret, config.refreshToken);
      return { token: refreshed.accessToken, config };
    }
  }

  return { token: config.accessToken, config };
}

// ── HTTP client ───────────────────────────────────────────────────────────────

/** Low-level request — accepts an already-resolved token and config to avoid redundant DB reads. */
async function ficRequestWithToken<T>(
  token: string,
  config: FicConfig,
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
  path: string,
  body?: unknown,
  _retried = false,
): Promise<T> {
  const res = await fetch(`${FIC_BASE}${path}`, {
    method,
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: body !== undefined ? JSON.stringify(body) : undefined,
  });

  // On 401: force refresh and retry once
  if (res.status === 401 && !_retried) {
    if (!config.refreshToken) {
      throw new Error('FIC returned 401 and no refresh token is available.');
    }
    const refreshed = await doRefresh(config.clientId, config.clientSecret, config.refreshToken);
    return ficRequestWithToken<T>(refreshed.accessToken, config, method, path, body, true);
  }

  if (!res.ok) {
    throw new Error(`FIC API ${method} ${path} failed (${res.status}): ${await res.text()}`);
  }

  return res.json() as Promise<T>;
}

/** Convenience wrapper — resolves a fresh token then delegates to ficRequestWithToken. */
async function ficRequest<T>(
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
  path: string,
  body?: unknown,
): Promise<T> {
  const { token, config } = await getValidToken();
  return ficRequestWithToken<T>(token, config, method, path, body);
}

// ── Public API ────────────────────────────────────────────────────────────────

/**
 * Create an issued invoice in Fatture in Cloud.
 * Marked as already paid because this is called after a successful Stripe charge.
 *
 * @returns The FIC document id of the created invoice.
 */
export async function createInvoice(params: CreateInvoiceParams): Promise<number> {
  // Resolve token once and pass it directly to avoid a second DB read
  const { token, config } = await getValidToken();
  const amountEur = params.amountCents / 100;

  const payload = {
    data: {
      type: 'invoice',
      date: params.date,
      // Use gross prices so payment amount always equals invoice total regardless of VAT rate
      use_gross_prices: true,
      currency: { id: 'EUR', exchange_rate: '1.00000', symbol: '€' },
      language: { code: 'it', name: 'Italiano' },
      entity: params.entity,
      items_list: params.items,
      payments_list: [{ due_date: params.date, amount: amountEur, status: 'paid' }],
      notes: params.notes ?? '',
    },
  };

  const response = await ficRequestWithToken<{ data: { id: number } }>(
    token, config,
    'POST',
    `/c/${config.companyId}/issued_documents`,
    payload,
  );

  return response.data.id;
}

/**
 * Verify the current access token is valid by fetching the company info.
 * Used by the admin test-connection button.
 */
export async function testConnection(): Promise<{ ok: boolean; companyName?: string; error?: string }> {
  try {
    const config = await loadConfig();
    const res = await ficRequest<{ data: { name: string } }>(
      'GET',
      `/c/${config.companyId}/company/info`,
    );
    return { ok: true, companyName: res.data.name };
  } catch (err) {
    return { ok: false, error: err instanceof Error ? err.message : 'Unknown error' };
  }
}

/**
 * Build a FicEntity from company + user data.
 * Falls back to user name/email when company financial data is incomplete.
 */
export function buildEntity(opts: {
  companyName: string;
  userName?: string | null;
  email: string;
  vatNumber?: string | null;
  taxId?: string | null;
  address?: string | null;
}): FicEntity {
  return {
    name: opts.companyName || opts.userName || opts.email,
    email: opts.email,
    vat_number: opts.vatNumber ?? undefined,
    tax_code: opts.taxId ?? undefined,
    address_street: opts.address ?? undefined,
  };
}

/**
 * Build a single line item for a token-usage invoice.
 * VAT type id is read from DB config (vatId field).
 */
export async function buildTokenUsageItem(amountCents: number, periodLabel: string): Promise<FicLineItem> {
  const config = await loadConfig();
  return {
    name: `Scriba – Token usage ${periodLabel}`,
    qty: 1,
    gross_price: amountCents / 100,
    vat: { id: config.vatId },
  };
}

/**
 * Issue a monthly invoice for a company with one line per translated conversion.
 * Maps plain {name, amountEur} lines to FIC line items using the configured VAT id.
 * Returns the FIC document id.
 */
export async function issueCompanyInvoice(opts: {
  entity: FicEntity;
  date: string;
  lines: Array<{ name: string; amountEur: number }>;
  amountCents: number;
  notes?: string;
}): Promise<number> {
  const config = await loadConfig();
  const items: FicLineItem[] = opts.lines.map((l) => ({
    name: l.name,
    qty: 1,
    gross_price: l.amountEur,
    vat: { id: config.vatId },
  }));
  return createInvoice({
    date: opts.date,
    entity: opts.entity,
    items,
    amountCents: opts.amountCents,
    notes: opts.notes,
  });
}
