import { isInvoiceError, type InvoiceError } from '../errors.js'
import type { DocumentSpec, InvoiceDocumentResult } from '../types.js'
import type {
  MorningClientOptions,
  MorningCredentials,
  MorningDocumentResponse,
  MorningTokenResponse,
} from './types.js'

const DEFAULT_BASE_URL = 'https://api.morning.co.il/v2'
const DEFAULT_TOKEN_PATH = '/token'
const DEFAULT_DOCUMENT_PATH = '/doc'

export interface MorningClient {
  exchangeToken(credentials: MorningCredentials): Promise<string>
  createDocument(
    accessToken: string,
    credentials: MorningCredentials,
    spec: DocumentSpec,
  ): Promise<InvoiceDocumentResult>
}

export function createMorningClient(options: MorningClientOptions = {}): MorningClient {
  const fetchImpl = options.fetch ?? fetch
  const baseUrl = trimTrailingSlash(options.baseUrl ?? DEFAULT_BASE_URL)
  const tokenPath = ensureLeadingSlash(options.tokenPath ?? DEFAULT_TOKEN_PATH)
  const documentPath = ensureLeadingSlash(options.documentPath ?? DEFAULT_DOCUMENT_PATH)

  return {
    async exchangeToken(credentials: MorningCredentials): Promise<string> {
      try {
        const response = await fetchImpl(`${baseUrl}${tokenPath}`, {
          method: 'POST',
          headers: jsonHeaders(),
          body: JSON.stringify(credentials),
        })
        const payload = (await readJson(response)) as MorningTokenResponse | null

        if (!response.ok) {
          throw {
            code: 'CREDENTIAL_INVALID',
            message: `Morning authentication failed: ${extractMessage(payload) ?? `HTTP ${response.status}`}`,
          } satisfies InvoiceError
        }

        const token = extractToken(payload)
        if (!token) {
          throw {
            code: 'CREDENTIAL_INVALID',
            message: 'Morning authentication failed: missing bearer token',
          } satisfies InvoiceError
        }

        return token
      } catch (error) {
        throw toMorningNetworkError(error, 'Morning authentication failed')
      }
    },

    async createDocument(
      accessToken: string,
      credentials: MorningCredentials,
      spec: DocumentSpec,
    ): Promise<InvoiceDocumentResult> {
      try {
        const response = await fetchImpl(`${baseUrl}${documentPath}`, {
          method: 'POST',
          headers: {
            ...jsonHeaders(),
            Authorization: `Bearer ${accessToken}`,
            'x-company-id': credentials.companyId,
          },
          body: JSON.stringify(buildDocumentPayload(spec)),
        })
        const payload = (await readJson(response)) as MorningDocumentResponse | null

        if (!response.ok) {
          throw {
            code: response.status >= 500 ? 'PROVIDER_UNAVAILABLE' : 'PROVIDER_REJECTED',
            message: `Morning create-document failed: ${extractMessage(payload) ?? `HTTP ${response.status}`}`,
          } satisfies InvoiceError
        }

        return mapDocumentResult(payload)
      } catch (error) {
        throw toMorningNetworkError(error, 'Morning create-document failed')
      }
    },
  }
}

function buildDocumentPayload(spec: DocumentSpec): Record<string, unknown> {
  return {
    docType: spec.docType,
    currency: spec.currency,
    externalId: spec.idempotencyKey,
    client: {
      name: spec.customer.name,
      ...(spec.customer.email ? { emailAddress: spec.customer.email } : {}),
      ...(spec.customer.taxId ? { taxId: spec.customer.taxId } : {}),
    },
    income: spec.lineItems.map((item) => ({
      description: item.description,
      quantity: item.quantity,
      price: formatMinorUnits(item.unitAmountMinor),
      taxTreatment: item.taxTreatment,
      vatRateBasisPoints: item.vatRateBasisPoints,
    })),
  }
}

function mapDocumentResult(payload: MorningDocumentResponse | null): InvoiceDocumentResult {
  const documentId = toNonEmptyString(
    payload?.documentId ?? payload?.id ?? payload?.doc_id,
  )
  if (!documentId) {
    throw {
      code: 'PROVIDER_REJECTED',
      message: 'Morning create-document failed: missing document id',
    } satisfies InvoiceError
  }

  const documentNumber =
    toNonEmptyString(payload?.documentNumber ?? payload?.docNum ?? payload?.doc_number) ??
    documentId
  const documentUrl =
    toNonEmptyString(payload?.documentUrl ?? payload?.url) ?? ''

  return {
    documentId,
    documentNumber,
    documentUrl,
  }
}

function extractToken(payload: MorningTokenResponse | null): string | null {
  return (
    toNonEmptyString(payload?.access_token) ??
    toNonEmptyString(payload?.accessToken) ??
    toNonEmptyString(payload?.token) ??
    toNonEmptyString(payload?.data?.access_token) ??
    toNonEmptyString(payload?.data?.accessToken) ??
    toNonEmptyString(payload?.data?.token) ??
    null
  )
}

function extractMessage(payload: unknown): string | null {
  if (typeof payload !== 'object' || payload === null) return null

  const candidate = payload as {
    message?: unknown
    error?: unknown
    reason?: unknown
  }

  return (
    toNonEmptyString(candidate.message) ??
    nestedMessage(candidate.error) ??
    toNonEmptyString(candidate.reason) ??
    null
  )
}

function nestedMessage(value: unknown): string | null {
  if (typeof value === 'string') return value
  if (typeof value !== 'object' || value === null) return null

  const candidate = value as { message?: unknown }
  return toNonEmptyString(candidate.message) ?? null
}

async function readJson(response: Response): Promise<unknown | null> {
  const text = await response.text()
  if (text.length === 0) return null

  try {
    return JSON.parse(text) as unknown
  } catch {
    return { message: text }
  }
}

function toMorningNetworkError(error: unknown, prefix: string): InvoiceError {
  if (isInvoiceError(error)) return error
  if (error instanceof Error && error.message.length > 0) {
    return { code: 'PROVIDER_UNAVAILABLE', message: `${prefix}: ${error.message}` }
  }
  return { code: 'PROVIDER_UNAVAILABLE', message: `${prefix}: network unavailable` }
}

function formatMinorUnits(amountMinor: bigint): string {
  const negative = amountMinor < 0n
  const absolute = negative ? amountMinor * -1n : amountMinor
  const whole = absolute / 100n
  const fraction = absolute % 100n
  const formatted = `${whole}.${fraction.toString().padStart(2, '0')}`
  return negative ? `-${formatted}` : formatted
}

function toNonEmptyString(value: unknown): string | null {
  if (typeof value === 'string' && value.trim().length > 0) {
    return value
  }
  if (typeof value === 'number' && Number.isFinite(value)) {
    return String(value)
  }
  return null
}

function trimTrailingSlash(value: string): string {
  return value.endsWith('/') ? value.slice(0, -1) : value
}

function ensureLeadingSlash(value: string): string {
  return value.startsWith('/') ? value : `/${value}`
}

function jsonHeaders(): Record<string, string> {
  return {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  }
}
