use anyhow::Context;
use std::path::Path;

/// An `IndexHtml` represents the contents of an `index.html` file used to serve a web application.
///
/// This defines the static portion of your web application, typically generated by a tool like `dx`
/// in conjunction with wasm-bindgen.
///
/// This structure expects a well-formed HTML document split into several sections:
/// - `head_before_title`: The portion of the `<head>` section before the `<title
/// - `title`: The `<title>` tag and its contents.
/// - `head_after_title`: The portion of the `<head>` section after the `<title>` tag.
/// - `close_head`: The closing `</head>` tag and any content following it.
/// - `post_main`: The content following the main application container (e.g., `<div id="main">`).
/// - `after_closing_body_tag`: The content following the closing `</body>` tag.
///
/// These fields are not explicitly exposed as part of the API, but are critical for dioxus-fullstack
/// to properly inject server-rendered content and client-side bootstrapping scripts into the HTML.
///
/// The simplest HTML document that satisfies this structure is:
///
/// ```html
/// <html>
///     <head> </head>
///     <body>
///         <div id="main"></div>
///     </body>
/// </html>
/// ```
#[derive(Clone, Debug)]
pub struct IndexHtml {
    pub(crate) head_before_title: String,
    pub(crate) head_after_title: String,
    pub(crate) title: String,
    pub(crate) close_head: String,
    pub(crate) post_main: String,
    pub(crate) after_closing_body_tag: String,
}

impl IndexHtml {
    /// Create a new `IndexHtml` from the raw contents of an `index.html` file.
    ///
    /// This function will parse the `index.html` and split it into sections for easier manipulation.
    /// The `root_id` parameter specifies the id of the main application container (e.g., "main")
    /// which your app will render into.
    pub fn new(contents: &str, root_id: &str) -> Result<IndexHtml, anyhow::Error> {
        let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\""))
            .with_context(|| format!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page."))?;

        let post_main = post_main.split_once('>').with_context(|| {
            format!("Failed to find closing > after id=\"{root_id}\" in index.html.")
        })?;

        let (pre_main, post_main) = (
            pre_main.to_string() + &format!("id=\"{root_id}\"") + post_main.0 + ">",
            post_main.1.to_string(),
        );

        let (head, close_head) = pre_main.split_once("</head>").with_context(|| {
            format!("Failed to find closing </head> tag after id=\"{root_id}\" in index.html.")
        })?;

        let (head, close_head) = (head.to_string(), "</head>".to_string() + close_head);

        let (post_main, after_closing_body_tag) =
            post_main.split_once("</body>").with_context(|| {
                format!("Failed to find closing </body> tag after id=\"{root_id}\" in index.html.")
            })?;

        // Strip out the head if it exists
        let mut head_before_title = String::new();
        let mut head_after_title = head;
        let mut title = String::new();
        if let Some((new_head_before_title, new_title)) = head_after_title.split_once("<title>") {
            let (new_title, new_head_after_title) = new_title
                .split_once("</title>")
                .context("Failed to find closing </title> tag after <title> in index.html.")?;
            title = format!("<title>{new_title}</title>");
            head_before_title = new_head_before_title.to_string();
            head_after_title = new_head_after_title.to_string();
        }

        Ok(IndexHtml {
            head_before_title,
            head_after_title,
            title,
            close_head,
            post_main: post_main.to_string(),
            after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
        })
    }

    /// Load an `IndexHtml` from a file at the given path.
    ///
    /// This function reads the contents of the file and parses it into an `IndexHtml`.
    /// The `id` parameter specifies the id of the main application container (e.g., "main").
    pub fn from_file(path: &Path, id: &str) -> Result<IndexHtml, anyhow::Error> {
        let contents = std::fs::read_to_string(path)
            .with_context(|| format!("Failed to read index.html from {}", path.display()))?;
        IndexHtml::new(&contents, id)
    }

    /// Create a default `IndexHtml` suitable for server-side rendering (SSR) only applications.
    /// This will not include any customizations to the HTML nor any JavaScript to bootstrap a client-side app.
    pub fn ssr_only() -> Self {
        const DEFAULT: &str = r#"<!DOCTYPE html>
        <html>
            <head> </head>
            <body>
                <div id="main"></div>
            </body>
        </html>"#;

        Self::new(DEFAULT, "main").expect("Failed to load default index.html")
    }
}
