use jiff::tz::TimeZone;
use jiff::Timestamp;
use pleroma::Activities;
use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
use std::{
    collections::HashMap,
    env, fmt,
    fs::File,
    io::{self, BufReader, BufWriter},
    path::{Path, PathBuf},
    process::ExitCode,
};
use url::Url;

mod pleroma;

type BoxError = Box<dyn std::error::Error>;
type Mappings = HashMap<String, Option<Url>>;

static TZ: OnceLock<TimeZone> = OnceLock::new();
static MAPPINGS: OnceLock<Mappings> = OnceLock::new();

fn main() -> ExitCode {
    let Some(path) = env::args_os().skip(1).next().map(PathBuf::from) else {
        eprintln!("Usage: pleroma-archive path/to/pleroma-archive");
        return ExitCode::FAILURE;
    };

    let timezone = TimeZone::system();
    TZ.set(timezone).unwrap();

    match try_main(&path) {
        Ok(()) => ExitCode::SUCCESS,
        Err(err) => {
            eprintln!("Error: {err}");
            ExitCode::FAILURE
        }
    }
}

fn try_main(path: &Path) -> Result<(), BoxError> {
    let actor_path = path.join("actor.json");
    let outbox_path = path.join("outbox.json");
    let mappings_path = path.join("mappings.json");

    let file = BufReader::new(File::open(&actor_path)?);
    let actor: pleroma::Actor = serde_json::from_reader(file)?;

    let file = BufReader::new(File::open(&outbox_path)?);
    let activities: Activities = serde_json::from_reader(file)?;

    // Load mappings of ids to public URLs
    let mut mappings: Mappings = match File::open(&mappings_path) {
        Ok(file) => serde_json::from_reader(BufReader::new(file))?,
        Err(err) if err.kind() == io::ErrorKind::NotFound => HashMap::new(),
        Err(err) => return Err(err.into()),
    };

    let agent = ureq::AgentBuilder::new().redirects(0).build();

    let mut posts = Vec::with_capacity(activities.ordered_items.len());
    for item in &activities.ordered_items {
        match &item.object {
            pleroma::activity::ObjectUnion::ObjectClass(activity) => {
                let id: Url = item.id.parse()?;
                if !mappings.contains_key(id.as_str()) {
                    let response = match agent.head(id.as_str()).call() {
                        Ok(res) => res,
                        Err(ureq::Error::Status(status, _res)) => {
                            eprintln!("expected 3xx response, got {} for {}", status, id);
                            mappings.insert(item.id.clone(), None);
                            continue;
                        }
                        Err(err) => return Err(err.into()),
                    };
                    if !(300..400).contains(&response.status()) {
                        eprintln!(
                            "expected 3xx response, got {} for {}",
                            response.status(),
                            id
                        );
                        mappings.insert(item.id.clone(), None);
                        continue;
                    }
                    let Some(location) = response.header("location") else {
                        return Err("expected a Location header, but it's missing".into());
                    };

                    let url = id.join(location)?;
                    mappings.insert(item.id.clone(), Some(url));
                }

                posts.push(activity);
            }
            pleroma::activity::ObjectUnion::String(s) => {
                eprintln!("ObjectUnion::String: {s}");
                // TODO
            }
        }
    }

    let mappings_writer = BufWriter::new(File::create(&mappings_path)?);
    serde_json::to_writer_pretty(mappings_writer, &mappings)?;

    MAPPINGS.set(mappings).unwrap();

    // dbg!(&posts);

    println!(
        "{}",
        Page {
            title: "Pleroma Archive",
            actor: &actor,
            activities: &posts,
        }
    );

    Ok(())
}

markup::define! {
    Page<'a>(title: &'a str, actor: &'a pleroma::Actor, activities: &'a [&'a pleroma::Activity]) {
        @markup::doctype()
        html {
            head {
                meta[charset="utf-8"];
                meta[name="viewport", content="width=device-width, initial-scale=1"];
                title { @title }
                link[rel="stylesheet", type="text/css", href="style.css"];
            }
            body {
                @Header { title, actor }
                main {
                    @for activity in activities.iter().rev() {
                        @Activity { actor, activity }
                    }
                }
                @Footer {  }
            }
        }
    }

    Header<'a>(title: &'a str, actor: &'a pleroma::Actor) {
        header {
            h1 { @title }
            p { "This is a static archive of " @actor.username() }
            p { @markup::raw(&actor.summary) }
        }
    }

    Footer() {
        footer {
            "Generated by "
            a[href="https://forge.wezm.net/wezm/pleroma-archive"] { "pleroma-archive" }
        }
    }

    Activity<'a>(actor: &'a pleroma::Actor, activity: &'a pleroma::Activity) {
        div[class=activity_class(&activity.object_type)] {
            @Actor { actor }

            div[class="activity-content"] {
                a[href=MAPPINGS.get().unwrap().get(&activity.id).and_then(|url| url.as_ref().map(|url| url.as_str()))] {
                    time[datetime=&activity.published] { @activity.human_published() }
                }

                @if let Some(in_reply_to) = &activity.in_reply_to {
                    a[href=in_reply_to, class="activity-reply-to"] { "↩ reply to" }
                    " "
                }
                @markup::raw(&activity.content)
            }
        }
        hr;
    }

    Actor<'a>(actor: &'a pleroma::Actor) {
        @if let Some(icon) = &actor.icon {
            @if icon.icon_type == "Image" {
                img[src=&icon.url, alt=&actor.preferred_username, class="actor-icon"];
            }
        }
    }
}

fn activity_class(object_type: &pleroma::activity::OneOfType) -> &'static str {
    match object_type {
        pleroma::activity::OneOfType::Note => "activity activity-note",
        pleroma::activity::OneOfType::Question => "activity activity-question",
    }
}

impl pleroma::Activity {
    fn human_published(&self) -> String {
        let published = self
            .published
            .parse()
            .map(|timestamp: Timestamp| timestamp.to_zoned(TZ.get().unwrap().clone()))
            .expect("invalid published value");

        published.strftime("%d %b %Y").to_string()
    }
}

impl pleroma::Actor {
    fn username(&self) -> &str {
        self.webfinger
            .strip_prefix("acct:")
            .unwrap_or(&self.webfinger)
    }
}