import { and, eq, gt } from "drizzle-orm"
import { db } from "@/lib/db"
import { accounting_connections, rent_payments, expenses } from "@/lib/db/schema"
import { encrypt, decrypt } from "@/lib/crypto"
import { quickbooks } from "./quickbooks"
import { xero } from "./xero"
import type { AccountingProvider, OAuthTokens, Provider } from "./types"

export type { Provider } from "./types"

const PROVIDERS: Record<Provider, AccountingProvider> = { quickbooks, xero }

export function getProvider(id: string): AccountingProvider | null {
  return id === "quickbooks" || id === "xero" ? PROVIDERS[id] : null
}

export function listProviders() {
  return (Object.keys(PROVIDERS) as Provider[]).map((id) => ({
    id,
    label: PROVIDERS[id].label,
    configured: PROVIDERS[id].configured(),
  }))
}

/** Upsert an encrypted connection for (owner, provider). */
export async function saveConnection(ownerId: string, provider: Provider, tokens: OAuthTokens) {
  const values = {
    user_id: ownerId,
    provider,
    access_token: encrypt(tokens.accessToken),
    refresh_token: encrypt(tokens.refreshToken),
    expires_at: tokens.expiresAt,
    realm_id: tokens.realmId,
    org_name: tokens.orgName,
    status: "active" as const,
    last_error: null,
  }
  const existing = await db.query.accounting_connections.findFirst({
    where: and(eq(accounting_connections.user_id, ownerId), eq(accounting_connections.provider, provider)),
    columns: { id: true },
  })
  if (existing) {
    await db.update(accounting_connections).set({ ...values, updated_at: new Date().toISOString() }).where(eq(accounting_connections.id, existing.id))
  } else {
    await db.insert(accounting_connections).values(values)
  }
}

export async function getConnection(ownerId: string, provider: Provider) {
  return db.query.accounting_connections.findFirst({
    where: and(eq(accounting_connections.user_id, ownerId), eq(accounting_connections.provider, provider)),
  })
}

export async function listConnections(ownerId: string) {
  const rows = await db.query.accounting_connections.findMany({ where: eq(accounting_connections.user_id, ownerId) })
  // Never leak tokens to callers.
  return rows.map((r) => ({ provider: r.provider, orgName: r.org_name, status: r.status, lastSyncAt: r.last_sync_at, lastError: r.last_error }))
}

export async function disconnect(ownerId: string, provider: Provider) {
  await db.delete(accounting_connections).where(and(eq(accounting_connections.user_id, ownerId), eq(accounting_connections.provider, provider)))
}

/** Load tokens, refreshing (and persisting) if the access token is near expiry. */
async function getValidTokens(ownerId: string, provider: Provider): Promise<OAuthTokens | null> {
  const conn = await getConnection(ownerId, provider)
  if (!conn) return null
  let tokens: OAuthTokens = {
    accessToken: decrypt(conn.access_token),
    refreshToken: decrypt(conn.refresh_token),
    expiresAt: conn.expires_at,
    realmId: conn.realm_id,
    orgName: conn.org_name,
  }
  const nearExpiry = conn.expires_at && new Date(conn.expires_at).getTime() - Date.now() < 60_000
  if (nearExpiry) {
    tokens = await getProvider(provider)!.refresh(tokens)
    await saveConnection(ownerId, provider, tokens)
  }
  return tokens
}

/**
 * One-way sync: push rent income + expenses created since the last sync into the
 * connected provider. Uses a created_at watermark for idempotency (records synced
 * once are not re-pushed).
 */
export async function syncNow(ownerId: string, provider: Provider): Promise<{ income: number; expense: number }> {
  const prov = getProvider(provider)
  if (!prov) throw new Error("Unknown provider")
  const conn = await getConnection(ownerId, provider)
  if (!conn) throw new Error("Not connected")

  try {
    const tokens = (await getValidTokens(ownerId, provider))!
    const since = conn.last_sync_at ?? "1970-01-01T00:00:00.000Z"

    const paid = await db.query.rent_payments.findMany({
      where: and(eq(rent_payments.user_id, ownerId), eq(rent_payments.status, "paid"), gt(rent_payments.created_at, since)),
      columns: { id: true, amount: true, paid_date: true, due_date: true, tenant_id: true },
      with: { tenant: { columns: { first_name: true, last_name: true } } },
    })
    const exp = await db.query.expenses.findMany({
      where: and(eq(expenses.user_id, ownerId), gt(expenses.created_at, since)),
      columns: { id: true, amount: true, expense_date: true, category: true, vendor: true, description: true },
    })

    const income = await prov.pushIncome(
      tokens,
      paid.map((p) => ({
        externalId: p.id,
        date: (p.paid_date ?? p.due_date ?? "").slice(0, 10),
        amount: Number(p.amount),
        customerName: `${p.tenant?.first_name ?? ""} ${p.tenant?.last_name ?? ""}`.trim() || "Tenant",
        description: `Rent payment`,
      }))
    )
    const expense = await prov.pushExpense(
      tokens,
      exp.map((e) => ({
        externalId: e.id,
        date: (e.expense_date ?? "").slice(0, 10),
        amount: Number(e.amount),
        category: e.category ?? "General",
        vendor: e.vendor ?? null,
        description: e.description ?? e.category ?? "Expense",
      }))
    )

    await db
      .update(accounting_connections)
      .set({ last_sync_at: new Date().toISOString(), status: "active", last_error: null, updated_at: new Date().toISOString() })
      .where(eq(accounting_connections.id, conn.id))

    return { income: income.synced, expense: expense.synced }
  } catch (e) {
    const msg = (e as Error).message.slice(0, 500)
    await db.update(accounting_connections).set({ status: "error", last_error: msg, updated_at: new Date().toISOString() }).where(eq(accounting_connections.id, conn.id))
    throw new Error(msg)
  }
}
