import { fetchJSON } from "std::http"
import { env } from "std::system"

/** @module
  @summary FRED — U.S. macroeconomic time-series
  ## 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, GDP, and ~800k other series, each addressed by its `series_id`
  (e.g. `UNRATE`, `CPIAUCSL`, `FEDFUNDS`). This is the curated gold standard for *U.S.*
  macro; for non-U.S. or cross-country data use `std::data/finance/dbnomics`; for news use
  `std::data/finance/gdelt`.

  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`; a follow-up will add URL-secret redaction to statelog. If this matters for
  > your deployment, avoid enabling observability on runs that call FRED, or route statelog
  > to a trusted sink.

  ### 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 data series. `units` is the requested units *transform code* (e.g. "lin", "pch"),
    not the display label — use fredSeriesInfo for the display units. */
export type FredSeries = {
  seriesId: string;
  units: string;
  observations: FredObservation[]
}

/** Metadata about a FRED series. `units` here IS the human display label (e.g. "Percent"). */
export type FredSeriesInfo = {
  id: string;
  title: string;
  units: string;
  frequency: string;
  observationStart: string;
  observationEnd: string;
  notes: string
}

// Not preapproved: callers see both std::fred and the std::http::fetchJSON interrupt.
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). Pure. */
def appendParam(path: string, key: string, value: string): string {
  if (value == "") {
    return path
  }
  return "${path}&${key}=${encodeURIComponent(value)}"
}

/** Build the series/observations query path. Pure — no network. */
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. Pure — no network. */
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. Pure. */
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. Pure — total on missing/null input. */
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. Pure — total on null input. */
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. Pure. */
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. Pure. */
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}")
  }
}

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::http::fetchJSON> {
  """
  Fetch a U.S. macroeconomic time-series from FRED by its series id (e.g. "UNRATE" for the
  unemployment rate, "CPIAUCSL" for CPI, "FEDFUNDS" for the fed funds rate). Returns the
  series and a list of { date, value } observations, ordered oldest-first (value is null when
  missing). For the most recent data, set observationStart rather than relying on limit
  (limit caps from the oldest observation). Requires the FRED_API_KEY environment variable.

  @param seriesId - FRED series id, e.g. "UNRATE"
  @param observationStart - Optional earliest date, "YYYY-MM-DD" (empty for no bound)
  @param observationEnd - Optional latest date, "YYYY-MM-DD" (empty for no bound)
  @param frequency - Optional frequency aggregation code, e.g. "m", "q", "a" (null for native)
  @param units - Optional units transform code, e.g. "pch" for percent change (null for "lin")
  @param limit - Optional max observations from the oldest, 0 means no limit
  """
  const apiKey = env("FRED_API_KEY") ?? ""
  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 = fetchJSON(baseUrl: FRED_BASE, path: path, allowedDomains: FRED_DOMAINS)
  return fredObservationsFinalize(seriesId, result)
}

export def fredSeriesInfo(seriesId: string): Result<FredSeriesInfo> raises <std::fred, std::http::fetchJSON> {
  """
  Fetch metadata about a FRED series by its id: its human title, display units, frequency, and
  the date range it covers. Requires the FRED_API_KEY environment variable.

  @param seriesId - FRED series id, e.g. "UNRATE"
  """
  const apiKey = env("FRED_API_KEY") ?? ""
  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 = fetchJSON(baseUrl: FRED_BASE, path: path, allowedDomains: FRED_DOMAINS)
  return fredInfoFinalize(result)
}
