///|
struct Options {
  limit : Int
  comments : Int
  lang : String
  model : String
  raw : Bool
}

///|
struct DeepSeekMessage {
  content : String
} derive(FromJson, @debug.Debug)

///|
struct DeepSeekChoice {
  message : DeepSeekMessage
} derive(FromJson, @debug.Debug)

///|
struct DeepSeekResponse {
  choices : Array[DeepSeekChoice]
} derive(FromJson, @debug.Debug)

///|
async fn fetch_json(url : String) -> Json {
  let (response, body) = @http.get(url)
  guard response.code == 200 else {
    fail("GET \{url} returned HTTP \{response.code}")
  }
  body.json()
}

///|
async fn fetch_item(id : Int) -> HnItem {
  let json = fetch_json("https://hacker-news.firebaseio.com/v0/item/\{id}.json")
  @json.from_json(json)
}

///|
async fn fetch_top_story_ids() -> Array[Int] {
  let json = fetch_json("https://hacker-news.firebaseio.com/v0/topstories.json")
  @json.from_json(json)
}

///|
async fn fetch_hot_comments(
  story : HnItem,
  max_comments : Int,
) -> Array[CommentBrief] {
  match story.kids {
    None => []
    Some(kids) => {
      let limit = max_comments.min(kids.length())
      @async.with_task_group() <| group => {
        let tasks = kids[:limit].map(id => group.spawn(() => fetch_item(id)))
        let comments : Array[CommentBrief] = []
        for task in tasks {
          let item = task.wait()
          if item.is_visible() && item.text != None {
            comments.push(item.to_comment_brief())
          }
        }
        comments
      }
    }
  }
}

///|
async fn fetch_brief(options : Options) -> Array[StoryBrief] {
  let ids = fetch_top_story_ids()
  let limit = options.limit.min(ids.length())
  @async.with_task_group() <| group => {
    let tasks = ids[:limit].map(id => {
      group.spawn(() => {
        let item = fetch_item(id)
        if item.is_visible() && item.title != None {
          let comments = fetch_hot_comments(item, options.comments)
          Some(item.to_story_brief(comments))
        } else {
          None
        }
      })
    })
    let stories : Array[StoryBrief] = []
    for task in tasks {
      match task.wait() {
        Some(story) => stories.push(story)
        None => ()
      }
    }
    stories
  }
}

///|
async fn call_deepseek(
  api_key : String,
  model : String,
  prompt : String,
) -> String {
  let headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer \{api_key}",
  }
  let request = (
    {
      "model": model,
      "messages": [
        {
          "role": "system",
          "content": "You produce concise Hacker News technical briefings.",
        },
        { "role": "user", "content": prompt },
      ],
      "stream": false,
    } : Json).stringify()
  let (response, body) = @http.post(
    "https://api.deepseek.com/chat/completions",
    request,
    headers~,
  )
  let text = body.text()
  guard response.code == 200 else {
    fail("DeepSeek returned HTTP \{response.code}: \{text}")
  }
  let parsed : DeepSeekResponse = @json.from_json(@json.parse(text))
  guard parsed.choices is [first, ..] else {
    fail("DeepSeek returned no choices")
  }
  first.message.content
}

///|
async fn run_brief(options : Options) -> String {
  let stories = fetch_brief(options)
  guard @env.get_env_var("DEEPSEEK_API_KEY") is Some(api_key) else {
    fail("DEEPSEEK_API_KEY is required.")
  }
  let summary = call_deepseek(
    api_key,
    options.model,
    summary_prompt(stories, options.lang),
  )
  render_markdown(
    stories,
    summary,
    model_label=options.model,
    show_context=options.raw,
  )
}

///|
fn read_options() -> Options raise {
  let matches = @argparse.Command(
    "hn-brief",
    about=(
      #|Fetch Hacker News top stories, ranked comments, and summarize them with DeepSeek.
    ),
    flags=[
      FlagArg(
        "raw",
        about="include the HN stories and comments sent to DeepSeek",
      ),
    ],
    options=[
      OptionArg("limit", about="number of HN stories to fetch"),
      OptionArg("comments", about="ranked comments to fetch per story"),
      OptionArg("lang", about="summary language label"),
      OptionArg("model", about="DeepSeek model name"),
    ],
  ).parse()
  {
    limit: match matches.values {
      { "limit": [value], .. } => @string.parse_int(value)
      _ => 10
    },
    comments: match matches.values {
      { "comments": [value], .. } => @string.parse_int(value)
      _ => 3
    },
    lang: match matches.values {
      { "lang": [value], .. } => value
      _ => "Simplified Chinese"
    },
    model: match matches.values {
      { "model": [value], .. } => value
      _ => "deepseek-v4-flash"
    },
    raw: match matches.flags {
      { "raw": true, .. } => true
      _ => false
    },
  }
}

///|
async fn main {
  let options = read_options()
  println(run_brief(options))
}