import type { ESignAdapter, ESignCredentials, ESignTokens, SendParams } from "./types"
import { esignRedirectUri } from "./types"

// DocuSign eSignature via per-landlord OAuth (Authorization Code Grant).
// The OPERATOR registers one DocuSign app and sets these; each LANDLORD then
// connects their own DocuSign account through it.
//   DOCUSIGN_CLIENT_ID / DOCUSIGN_CLIENT_SECRET  — the app's integration key + secret
//   DOCUSIGN_OAUTH_BASE — "account-d.docusign.com" (demo) or "account.docusign.com" (prod)
const CLIENT_ID = process.env.DOCUSIGN_CLIENT_ID ?? ""
const CLIENT_SECRET = process.env.DOCUSIGN_CLIENT_SECRET ?? ""
const OAUTH_BASE = (process.env.DOCUSIGN_OAUTH_BASE ?? "account-d.docusign.com").replace(/^https?:\/\//, "").replace(/\/+$/, "")

function basicAuth() {
  return "Basic " + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")
}

function extOf(name: string): string {
  const e = name.split(".").pop()?.toLowerCase()
  return e && /^(pdf|docx?|png|jpe?g)$/.test(e) ? e : "pdf"
}

/** Map a DocuSign envelope/event status string to one of our terminal statuses. */
function mapStatus(raw: string): "signed" | "declined" | "voided" | null {
  const s = raw.toLowerCase()
  if (s.includes("completed") || s.includes("signed")) return "signed"
  if (s.includes("declined")) return "declined"
  if (s.includes("voided")) return "voided"
  return null
}

async function tokenRequest(form: Record<string, string>): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {
  const res = await fetch(`https://${OAUTH_BASE}/oauth/token`, {
    method: "POST",
    headers: { Authorization: basicAuth(), "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
    body: new URLSearchParams(form),
  })
  if (!res.ok) throw new Error(`DocuSign token error ${res.status}: ${(await res.text()).slice(0, 300)}`)
  return res.json()
}

async function userInfo(accessToken: string): Promise<{ accountId: string | null; baseUri: string | null; accountName: string | null }> {
  const res = await fetch(`https://${OAUTH_BASE}/oauth/userinfo`, {
    headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
  })
  if (!res.ok) throw new Error(`DocuSign userinfo ${res.status}`)
  const j = (await res.json()) as { accounts?: { account_id: string; base_uri: string; account_name: string; is_default: boolean }[] }
  const acct = j.accounts?.find((a) => a.is_default) ?? j.accounts?.[0]
  return { accountId: acct?.account_id ?? null, baseUri: acct?.base_uri ?? null, accountName: acct?.account_name ?? null }
}

/** REST API base for the envelopes API, e.g. https://na3.docusign.net/restapi/v2.1/accounts/<id> */
function apiBase(creds: ESignCredentials): string {
  return `${(creds.baseUri ?? "").replace(/\/+$/, "")}/restapi/v2.1/accounts/${creds.accountId}`
}

export const docusign: ESignAdapter = {
  id: "docusign",
  label: "DocuSign",
  kind: "oauth",
  available: () => Boolean(CLIENT_ID && CLIENT_SECRET),

  getAuthUrl(state) {
    const p = new URLSearchParams({
      response_type: "code",
      // `extended` is required to receive a refresh token.
      scope: "signature extended",
      client_id: CLIENT_ID,
      redirect_uri: esignRedirectUri("docusign"),
      state,
    })
    return `https://${OAUTH_BASE}/oauth/auth?${p.toString()}`
  },

  async exchangeCode(code): Promise<ESignTokens> {
    const t = await tokenRequest({ grant_type: "authorization_code", code })
    const info = await userInfo(t.access_token)
    return {
      accessToken: t.access_token,
      refreshToken: t.refresh_token,
      expiresAt: new Date(Date.now() + t.expires_in * 1000).toISOString(),
      accountId: info.accountId,
      baseUri: info.baseUri,
      accountName: info.accountName,
    }
  },

  async refresh(refreshToken): Promise<ESignTokens> {
    const t = await tokenRequest({ grant_type: "refresh_token", refresh_token: refreshToken })
    // Account id / base uri are stable across refreshes; the resolver re-attaches them.
    return {
      accessToken: t.access_token,
      refreshToken: t.refresh_token,
      expiresAt: new Date(Date.now() + t.expires_in * 1000).toISOString(),
      accountId: null,
      baseUri: null,
      accountName: null,
    }
  },

  async connectApiKey(): Promise<ESignTokens> {
    throw new Error("DocuSign connects via OAuth, not an API key")
  },

  async send(creds, p: SendParams) {
    const envelope = {
      emailSubject: p.subject,
      status: "sent",
      documents: [{ documentBase64: p.document.toString("base64"), name: p.documentName, fileExtension: extOf(p.documentName), documentId: "1" }],
      recipients: {
        signers: [
          {
            email: p.signerEmail,
            name: p.signerName,
            recipientId: "1",
            routingOrder: "1",
            tabs: { signHereTabs: [{ documentId: "1", pageNumber: "1", xPosition: "100", yPosition: "650" }] },
          },
        ],
      },
      // Envelope-level Connect: DocuSign pings our webhook on completion so we
      // pull the authoritative status. No account-level Connect config needed.
      eventNotification: {
        url: p.webhookUrl,
        loggingEnabled: "true",
        requireAcknowledgment: "true",
        envelopeEvents: [
          { envelopeEventStatusCode: "completed" },
          { envelopeEventStatusCode: "declined" },
          { envelopeEventStatusCode: "voided" },
        ],
        eventData: { version: "restv2.1" },
      },
    }
    const res = await fetch(`${apiBase(creds)}/envelopes`, {
      method: "POST",
      headers: { Authorization: `Bearer ${creds.accessToken}`, "Content-Type": "application/json" },
      body: JSON.stringify(envelope),
    })
    if (!res.ok) throw new Error(`DocuSign ${res.status}: ${(await res.text()).slice(0, 300)}`)
    const j = (await res.json()) as { envelopeId?: string }
    if (!j.envelopeId) throw new Error("DocuSign did not return an envelopeId")
    return { externalId: j.envelopeId }
  },

  peekExternalId(body): string | null {
    try {
      const j = JSON.parse(body) as { data?: { envelopeId?: string } }
      return j.data?.envelopeId ?? null
    } catch {
      return null
    }
  },

  // We never trust the webhook body's status. Instead we fetch the envelope from
  // DocuSign with the owner's own OAuth token — a forged webhook can at most make
  // us re-read the real status, never fabricate a "signed".
  async verifyAndGetStatus(creds, externalId) {
    const res = await fetch(`${apiBase(creds)}/envelopes/${externalId}`, {
      headers: { Authorization: `Bearer ${creds.accessToken}`, Accept: "application/json" },
    })
    if (!res.ok) return null
    const j = (await res.json()) as { status?: string }
    return j.status ? mapStatus(j.status) : null
  },

  async getSignedDocument(creds, externalId): Promise<Buffer | null> {
    const res = await fetch(`${apiBase(creds)}/envelopes/${externalId}/documents/combined`, {
      headers: { Authorization: `Bearer ${creds.accessToken}`, Accept: "application/pdf" },
    })
    if (!res.ok) return null
    return Buffer.from(await res.arrayBuffer())
  },
}
