library(atrrr)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(readr)
library(stringr)

collected_at <- Sys.time()

get_candidate_posts <- function(candidate_label, query) {
  raw_posts <- search_post(q = query, limit = 150, sort = "latest")

  if (is.null(raw_posts) || nrow(raw_posts) == 0) {
    warning("No posts found for: ", candidate_label)
    return(tibble())
  }

  post_times <- vapply(
    raw_posts$post_data,
    function(post) {
      if (is.null(post$createdAt)) NA_character_
      else as.character(post$createdAt)
    },
    character(1)
  )

  raw_posts |>
    mutate(timestamp_utc = post_times) |>
    transmute(
      post_id = uri,
      text = text,
      author_handle = author_handle,
      timestamp_utc = timestamp_utc,
      likes = like_count,
      reposts = repost_count,
      replies = reply_count,
      candidate = candidate_label
    ) |>
    distinct(post_id, .keep_all = TRUE)
}

becerra <- get_candidate_posts(
  "Becerra",
  "Xavier Becerra California governor"
)
## ℹ Found 5351 posts that fit the query
## ⠙ Got 97 posts, but there is more.. [639ms]⠹ Got 197 posts, but there is more.. [913ms]                                             ℹ Parsing 197 results.✔ Got 197 results. All done!
hilton <- get_candidate_posts(
  "Hilton",
  "Steve Hilton California governor"
)
## ℹ Found 3766 posts that fit the query
## ⠙ Got 98 posts, but there is more.. [261ms]⠹ Got 193 posts, but there is more.. [446ms]                                             ℹ Parsing 193 results.✔ Got 193 results. All done!
posts <- bind_rows(becerra, hilton) |>
  distinct(post_id, .keep_all = TRUE)

dir.create("data", showWarnings = FALSE)
write_excel_csv(posts, "data/bluesky_candidates_raw.csv")

library(tidytext)
library(tidyr)
library(stringr)

posts <- read_csv("data/bluesky_candidates_raw.csv", show_col_types = FALSE) |>
  mutate(
    candidate = case_when(
      str_detect(str_to_lower(candidate), "becerra|xavier") ~ "Becerra",
      str_detect(str_to_lower(candidate), "hilton|steve") ~ "Hilton",
      TRUE ~ NA_character_
    ),
    text = coalesce(text, "")
  ) |>
  filter(!is.na(candidate)) |>
  distinct(post_id, .keep_all = TRUE)

# Flag likely unrelated Hilton references for an aggregate cleaning audit.
posts <- posts |>
  mutate(
    hotel_noise = str_detect(
      str_to_lower(text),
      "paris\\s+hilton|hilton\\s+hotels?|hotels?\\s+hilton|hilton\\s+honors|hilton\\s+garden\\s+inn"
    )
  )

clean_posts <- posts |>
  filter(!hotel_noise)

# Audit totals only; do not publish handles or individual posts.
cleaning_audit <- posts |>
  count(candidate, hotel_noise, name = "posts")

write_excel_csv(cleaning_audit, "data/cleaning_audit_aggregate.csv")

# Count Bing-positive and Bing-negative words in each post.
bing_counts <- clean_posts |>
  select(post_id, candidate, text) |>
  unnest_tokens(word, text) |>
  anti_join(stop_words, by = "word") |>
  inner_join(get_sentiments("bing"), by = "word") |>
  count(post_id, candidate, sentiment, name = "n") |>
  pivot_wider(names_from = sentiment, values_from = n, values_fill = 0)

scored_posts <- clean_posts |>
  select(post_id, candidate, text) |>
  left_join(bing_counts, by = c("post_id", "candidate")) |>
  mutate(
    positive = coalesce(positive, 0L),
    negative = coalesce(negative, 0L),
    net = positive - negative,
    code_stance = case_when(
      net > 0 ~ "support",
      net < 0 ~ "oppose",
      TRUE ~ "neutral_unclear"
    )
  )

candidate_summary <- scored_posts |>
  group_by(candidate) |>
  summarise(
    posts = n(),
    positive_rate = mean(code_stance == "support"),
    negative_rate = mean(code_stance == "oppose"),
    neutral_unclear_rate = mean(code_stance == "neutral_unclear"),
    mean_net_bing = mean(net),
    .groups = "drop"
  )

write_excel_csv(candidate_summary, "data/candidate_summary_aggregate.csv")

#Ai Prompt Write R code for my California governor Bluesky-post assignment. My CSV has one row per post and these relevant columns: post_id (post URI), text (post text), candidate (either “Becerra” or “Hilton”), and may also include author_handle, timestamp_utc, likes, reposts, and replies. First flag and remove posts about “Paris Hilton,” Hilton Hotels, or clearly hotel-related Hilton terms. Include an aggregate cleaning audit. Then use tidytext and the Bing sentiment lexicon to count positive and negative words per post and summarize the results by candidate. Use net Bing sentiment = positive word count minus negative word count, and label positive net as support, negative net as oppose, and zero as neutral/unclear, while clearly explaining that lexical sentiment is not the same as candidate stance. Make a reproducible random sample of up to 10 posts per candidate for manual stance labeling, and provide code to compare those labels with the code labels using an aggregate confusion table and accuracy. Finally, use positive share as the prediction rule: calculate each candidate’s positive-post rate, normalize the two rates to sum to 100%, and call that the projected two-candidate vote share. Bootstrap posts within each candidate 2,000 times and report the 95% interval for Becerra’s projected share. Do not invent results. Do not publish usernames, post text, or individual-level records; output aggregate results only. Include package imports, comments, and code that I can run in RStudio.

set.seed(4103)

set.seed(4103)

label_sample <- scored_posts |>
  group_by(candidate) |>
  group_modify(~ slice_sample(.x, n = min(10, nrow(.x)))) |>
  ungroup() |>
  transmute(
    post_id,
    candidate,
    text,
    manual_stance = ""
  )

if (!file.exists("data/manual_labels.csv")) {
  write_excel_csv(label_sample, "data/manual_labels.csv")
}

manual_labels <- read_csv("data/manual_labels.csv", show_col_types = FALSE)

validation <- scored_posts |>
  select(post_id, candidate, code_stance) |>
  inner_join(
    manual_labels |> select(post_id, manual_stance),
    by = "post_id"
  ) |>
  filter(manual_stance %in% c("support", "oppose", "neutral_unclear"))

validation_accuracy <- validation |>
  summarise(
    labeled_posts = n(),
    accuracy = mean(code_stance == manual_stance)
  )

confusion_table <- validation |>
  count(manual_stance, code_stance, name = "posts")
  
  table(scored_posts$candidate)
## 
## Becerra  Hilton 
##     197     159
  # Calculate each candidate's positive-post rate,
# then scale the two rates to add up to 100%.
get_positive_share <- function(data) {
  rates <- data |>
    group_by(candidate) |>
    summarise(
      positive_rate = mean(code_stance == "support"),
      .groups = "drop"
    )

  candidates <- c("Becerra", "Hilton")

  if (!all(candidates %in% rates$candidate)) {
    stop("Your data needs posts for both Becerra and Hilton.")
  }

  rates_named <- setNames(rates$positive_rate, rates$candidate)
  rates_named <- rates_named[candidates]

  if (sum(rates_named) == 0) {
    return(setNames(c(NA_real_, NA_real_), candidates))
  }

  rates_named / sum(rates_named)
}

# The result from your collected posts
point_share <- get_positive_share(scored_posts)

if (anyNA(point_share)) {
  stop("Neither candidate has positive-scored posts, so positive share can't be calculated.")
}

predicted_winner <- if (
  abs(point_share[["Becerra"]] - point_share[["Hilton"]]) < 1e-12
) {
  "Tie"
} else {
  names(which.max(point_share))
}

# Bootstrap posts within each candidate
set.seed(4103)
bootstrap_draws <- 2000

becerra_bootstrap_shares <- replicate(bootstrap_draws, {
  bootstrap_posts <- scored_posts |>
    group_by(candidate) |>
    slice_sample(prop = 1, replace = TRUE) |>
    ungroup()

  get_positive_share(bootstrap_posts)[["Becerra"]]
})

valid_draws <- becerra_bootstrap_shares[
  is.finite(becerra_bootstrap_shares)
]

if (length(valid_draws) == 0) {
  stop("No valid bootstrap results. Check your candidate and stance labels.")
}

becerra_interval <- quantile(
  valid_draws,
  probs = c(0.025, 0.975),
  names = FALSE
)

# Display the aggregate result
prediction_result <- tibble(
  rule = "Positive share",
  predicted_winner = predicted_winner,
  becerra_projected_share_pct = round(100 * point_share[["Becerra"]], 1),
  hilton_projected_share_pct = round(100 * point_share[["Hilton"]], 1),
  becerra_95_interval_low_pct = round(100 * becerra_interval[1], 1),
  becerra_95_interval_high_pct = round(100 * becerra_interval[2], 1),
  bootstrap_draws_used = length(valid_draws)
)

print(prediction_result)
## # A tibble: 1 × 7
##   rule           predicted_winner becerra_projected_sha…¹ hilton_projected_sha…²
##   <chr>          <chr>                              <dbl>                  <dbl>
## 1 Positive share Hilton                              32.9                   67.1
## # ℹ abbreviated names: ¹​becerra_projected_share_pct,
## #   ²​hilton_projected_share_pct
## # ℹ 3 more variables: becerra_95_interval_low_pct <dbl>,
## #   becerra_95_interval_high_pct <dbl>, bootstrap_draws_used <int>
# paste your prediction and bootstrap code here