import { connectorFetch } from "std::data/connector"
import { env } from "std::system"

/** @module
  @summary Read FRED economic series and their metadata.
  ## FRED — U.S. macroeconomic time-series

  Fetch U.S. economic data from the Federal Reserve Bank of St. Louis
  [FRED API](https://fred.stlouisfed.org/docs/api/fred/): interest rates, CPI (inflation),
  unemployment, and GDP. Use a series ID such as `UNRATE`, `CPIAUCSL`,
  or `FEDFUNDS`. For other providers, see `std::data/finance/dbnomics`.

  Returns a series as a list of `{ date, value }` observations (a missing value is `null`),
  oldest-first, plus a `fredSeriesInfo` call for a series' title/display-units/frequency/coverage.

  ### Setup

  Requires a free API key. Get one at https://fred.stlouisfed.org/docs/api/api_key.html and
  set it as the `FRED_API_KEY` environment variable.

  > **Note:** FRED's API v1 accepts the key only as a URL query parameter, so when
  > `observability` is enabled the key appears in statelog `fetchJSON` interrupt payloads
  > (the URL is logged). This is a general property of any secret passed in a URL with
  > `std::http`. Use a trusted statelog destination for runs that call FRED.

  ### Usage

  ```ts
  import { fredSeries } from "std::data/finance/fred"

  node main() {
    const series = fredSeries("UNRATE", "2024-01-01") catch { seriesId: "", units: "", observations: [] }
    for (obs in series.observations) {
      print("${obs.date}: ${obs.value}")
    }
  }
  ```
*/

effect std::fred { seriesId: string }

/** A single FRED observation. `value` is null when FRED reports the value as missing ("."). */
export type FredObservation = {
  date: string;
  value: number | null
}

/** A FRED series whose `units` is a transform code, such as "lin" or "pch".
    Use `fredSeriesInfo` for display units. */
export type FredSeries = {
  seriesId: string;
  units: string;
  observations: FredObservation[]
}

/** Metadata about a FRED series, with display units such as "Percent". */
export type FredSeriesInfo = {
  id: string;
  title: string;
  units: string;
  frequency: string;
  observationStart: string;
  observationEnd: string;
  notes: string
}

// The std::fred interrupt gates the call; the fetch is approved at the call site
// (an outer handler can still reject std::http::fetchJSON).
const FRED_BASE = "https://api.stlouisfed.org/fred/"
const FRED_DOMAINS = ["api.stlouisfed.org"]

/** FRED units-transform codes (the `units` request parameter). `null` = native ("lin"). */
export type FredUnits = "lin" | "chg" | "ch1" | "pch" | "pc1" | "pca" | "cch" | "cca" | "log"

/** FRED frequency-aggregation codes (the `frequency` request parameter). `null` = native.
    Includes the weekly-ending variants (wef, weth, …). */
export type FredFrequency = "d" | "w" | "bw" | "m" | "q" | "sa" | "a" | "wef" | "weth" | "wetu" | "wew" | "wetdt" | "wem" | "wesun"

/* Append "&key=value" only when value is non-empty (value is URL-encoded). */
def appendParam(path: string, key: string, value: string): string {
  if (value == "") {
    return path
  }
  return "${path}&${key}=${encodeURIComponent(value)}"
}

/* Build the series/observations query path. */
def buildFredObservationsPath(seriesId: string, apiKey: string, observationStart: string, observationEnd: string, frequency: string, units: string, limit: number): string {
  let path = "series/observations?series_id=${encodeURIComponent(seriesId)}&api_key=${encodeURIComponent(apiKey)}&file_type=json"
  path = appendParam(path, "observation_start", observationStart)
  path = appendParam(path, "observation_end", observationEnd)
  path = appendParam(path, "frequency", frequency)
  path = appendParam(path, "units", units)
  let limitStr = ""
  if (limit > 0) {
    limitStr = "${limit}"
  }
  return appendParam(path, "limit", limitStr)
}

/* Build the series (metadata) query path. */
def buildFredSeriesPath(seriesId: string, apiKey: string): string {
  return "series?series_id=${encodeURIComponent(seriesId)}&api_key=${encodeURIComponent(apiKey)}&file_type=json"
}

/* Convert a FRED value string to a number, or null for the missing marker "." or a non-number. */
def toFredValue(raw: string): number | null {
  if (raw == ".") {
    return null
  }
  const n = Number(raw)
  if (isNaN(n)) {
    return null
  }
  return n
}

/* Reshape a series/observations response. */
def parseFredObservations(seriesId: string, raw: any): FredSeries {
  const r: any = raw ?? {}
  const rawObs = r.observations ?? []
  const observations = map(rawObs) as obs {
    return { date: obs.date ?? "", value: toFredValue(obs.value ?? ".") }
  }
  return { seriesId: seriesId, units: r.units ?? "", observations: observations }
}

/* Reshape a series (metadata) response, taking the first series. */
def parseFredInfo(raw: any): FredSeriesInfo {
  const r: any = raw ?? {}
  const list = r.seriess ?? []
  const info: any = list[0] ?? {}
  return {
    id: info.id ?? "",
    title: info.title ?? "",
    units: info.units ?? "",
    frequency: info.frequency ?? "",
    observationStart: info.observation_start ?? "",
    observationEnd: info.observation_end ?? "",
    notes: info.notes ?? ""
  }
}

/* Finalize an observations fetch into a Result. */
def fredObservationsFinalize(seriesId: string, fetchResult: any): Result {
  return match (fetchResult) {
    success(body) => {
      if (((body.observations ?? null) == null)) {
        return failure("FRED returned no observations for '${seriesId}' (check the series id)")
      }
      return success(parseFredObservations(seriesId, body))
    }
    failure(err) => failure("FRED request failed: ${err}")
  }
}

/* Finalize a series-info fetch into a Result. */
def fredInfoFinalize(fetchResult: any): Result {
  return match (fetchResult) {
    success(body) => {
      const list = body.seriess ?? []
      if (list.length == 0) {
        return failure("FRED returned no series metadata (check the series id)")
      }
      return success(parseFredInfo(body))
    }
    failure(err) => failure("FRED request failed: ${err}")
  }
}

// std::env is in the raises clauses below even though both reads are
// approved at the site: `with approve` answers the interrupt at runtime,
// it does not discharge the effect.
/**
  Set the observation start date when you want recent data. A limit takes
  observations from the oldest end of the selected range. Missing source
  values become null.
*/
@capabilities(network)
export def fredSeries(seriesId: string, observationStart: string = "", observationEnd: string = "", frequency: FredFrequency | null = null, units: FredUnits | null = null, limit: number = 0): Result<FredSeries> raises <std::fred, std::env, std::http::fetchJSON> {
  """
  Fetch observations for a FRED economic series, ordered oldest first.
  Requires FRED_API_KEY. For the most recent data, set observationStart
  rather than relying on limit, which keeps the oldest observations.

  @param seriesId - A FRED series ID such as "UNRATE" or "CPIAUCSL".
  @param observationStart - The earliest YYYY-MM-DD date, or empty for no lower bound.
  @param observationEnd - The latest YYYY-MM-DD date, or empty for no upper bound.
  @param frequency - The aggregation frequency, or null to keep the native frequency.
  @param units - The units transform, such as "pch" for percent change, or null for native values.
  @param limit - The maximum observations from the oldest, or zero for the API default.
  """
  const fredKey = env("FRED_API_KEY") with approve
  const apiKey = fredKey ?? ""
  if (apiKey == "") {
    return failure("FRED_API_KEY is not set. Get a free key at https://fred.stlouisfed.org/docs/api/api_key.html")
  }
  return interrupt std::fred("Fetch this FRED series?", { seriesId: seriesId })
  const path = buildFredObservationsPath(seriesId, apiKey, observationStart, observationEnd, frequency ?? "", units ?? "", limit)
  // Bind the fetch to its own statement so the std::http::fetchJSON interrupt fires at
  // statement level (resumable), not inside a call argument.
  const result = connectorFetch(FRED_BASE, FRED_DOMAINS, path) with approve
  return fredObservationsFinalize(seriesId, result)
}

@capabilities(network)
export def fredSeriesInfo(seriesId: string): Result<FredSeriesInfo> raises <std::fred, std::env, std::http::fetchJSON> {
  """
  Fetch a FRED series' title, display units, frequency, and coverage dates.
  Requires FRED_API_KEY.

  @param seriesId - A FRED series ID such as "UNRATE".
  """
  const fredKey = env("FRED_API_KEY") with approve
  const apiKey = fredKey ?? ""
  if (apiKey == "") {
    return failure("FRED_API_KEY is not set. Get a free key at https://fred.stlouisfed.org/docs/api/api_key.html")
  }
  return interrupt std::fred("Fetch this FRED series metadata?", { seriesId: seriesId })
  const path = buildFredSeriesPath(seriesId, apiKey)
  const result = connectorFetch(FRED_BASE, FRED_DOMAINS, path) with approve
  return fredInfoFinalize(result)
}
