use std::{
    fs,
    net::{IpAddr, Ipv4Addr},
    path::PathBuf,
    process,
};

use anyhow::{bail, Context, Result};
use log::{error, info, warn};
use url::Url;

use crate::{cli::OpenCommand, rofi, state::State, task};

pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> {
    match command {
        OpenCommand::Review => {
            for project in task::Project::all().context("Failed to get all project files")? {
                if project.is_touched() {
                    info!("Reviewing project: '{}'", project.to_project_display());
                    open_in_browser(project, state, None).with_context(|| {
                        format!(
                            "Failed to open project ('{}') in Firefox",
                            project.to_project_display()
                        )
                    })?;
                    project.untouch().with_context(|| {
                        format!(
                            "Failed to untouch project ('{}')",
                            project.to_project_display()
                        )
                    })?;
                }
            }
        }
        OpenCommand::Project { project, url } => {
            project.touch().context("Failed to touch project")?;
            open_in_browser(&project, state, url).with_context(|| {
                format!("Failed to open project: {}", project.to_project_display())
            })?;
        }
        OpenCommand::Select { url } => {
            let selected_project: task::Project = task::Project::from_project_string(
                &rofi::select(
                    task::Project::all()
                        .context("Failed to get all registered projects")?
                        .iter()
                        .map(task::Project::to_project_display)
                        .collect::<Vec<_>>()
                        .as_slice(),
                )
                .context("Failed to get selected project")?,
            )
            .expect("This should work, as we send only projects in");

            selected_project
                .touch()
                .context("Failed to touch project")?;

            open_in_browser(&selected_project, state, url).context("Failed to open project")?;
        }
        OpenCommand::ListTabs { project } => {
            let project = if let Some(p) = project {
                p
            } else if let Some(p) =
                task::Project::get_current().context("Failed to get currently focused project")?
            {
                p
            } else {
                bail!("You need to either supply a project or have a project active!");
            };

            let session_store = project.get_sessionstore().with_context(|| {
                format!(
                    "Failed to get session store for project: '{}'",
                    project.to_project_display()
                )
            })?;

            let selected = session_store
                .windows
                .iter()
                .map(|w| w.selected)
                .collect::<Vec<_>>();

            let tabs = session_store
                .windows
                .iter()
                .flat_map(|window| window.tabs.iter())
                .map(|tab| tab.entries.get(tab.index - 1).expect("This should be Some"))
                .collect::<Vec<_>>();

            for (index, entry) in tabs.iter().enumerate() {
                let index = index + 1;
                let is_selected = {
                    if selected.contains(&index) {
                        "🔻 "
                    } else {
                        "   "
                    }
                };
                println!("{}{}", is_selected, entry.url);
            }
        }
    }
    Ok(())
}

fn open_in_browser(
    selected_project: &task::Project,
    state: &mut State,
    url: Option<Url>,
) -> Result<()> {
    let old_project: Option<task::Project> =
        task::Project::get_current().context("Failed to get currently active project")?;
    let old_task: Option<task::Task> =
        task::Task::get_current(state).context("Failed to get currently active task")?;

    selected_project.activate().with_context(|| {
        format!(
            "Failed to active project: '{}'",
            selected_project.to_project_display()
        )
    })?;

    let tracking_task = {
        let all_tasks = selected_project.get_tasks(state).with_context(|| {
            format!(
                "Failed to get assoctiated tasks for project: '{}'",
                selected_project.to_project_display()
            )
        })?;

        let tracking_task = all_tasks.into_iter().find(|t| {
            let maybe_desc = t.description(state);
            if let Ok(desc) = maybe_desc {
                desc == "tracking"
            } else {
                error!(
                    "Getting task description returned error: {}",
                    maybe_desc.expect_err("We already check for Ok")
                );
                false
            }
        });

        if let Some(task) = tracking_task {
            info!(
                "Starting task {} -> tracking",
                selected_project.to_project_display()
            );
            task.start(state)
                .with_context(|| format!("Failed to start task {task}"))?;
        }
        tracking_task
    };

    let status = {
        let mut args = vec!["-P".to_owned(), selected_project.to_project_display()];
        if let Some(url) = url {
            args.push(url.to_string());
        } else {
            let lock_file = dirs::home_dir()
                .expect("Exists")
                .join(".mozilla/firefox")
                .join(selected_project.to_project_display())
                .join("lock");

            if lock_file.exists() {
                let (ip, pid): (IpAddr, u32) = {
                    let link = fs::read_link(&lock_file).with_context(|| {
                        format!("Failed to readlink lock at '{}'", lock_file.display())
                    })?;

                    let (ip, pid) = link
                        .to_str()
                        .expect("Should work")
                        .split_once(':')
                        .expect("The split works");

                    (
                        ip.parse().expect("Should be a valid ip address"),
                        pid.parse().expect("Should be a valid pid"),
                    )
                };

                if ip != Ipv4Addr::new(127, 0, 0, 2) {
                    warn!("Your ip is weird..");
                }

                if PathBuf::from("/proc").join(pid.to_string()).exists() {
                    // Another Firefox instance has already been started for this project
                    // Add a buffer URL to force Firefox to open it in the already open instance
                    args.push("about:newtab".to_owned());
                } else {
                    // This project does not yet have another Firefox instance
                    // We do not need to add anything to the arguments, Firefox will open a new
                    // instance.
                }
            } else {
                // There is no lock file and thus no instance already open.
            }
        };

        process::Command::new("firefox")
            .args(args)
            .status()
            .context("Failed to start firefox")?
    };

    if !status.success() {
        error!("Firefox run exited with error.");
    }

    if let Some(task) = tracking_task {
        task.stop(state)
            .with_context(|| format!("Failed to stop task {task}"))?;
    }
    if let Some(task) = old_task {
        task.start(state)
            .with_context(|| format!("Failed to start task {task}"))?;
    }

    if let Some(project) = old_project {
        project.activate().with_context(|| {
            format!("Failed to active project {}", project.to_project_display())
        })?;
    } else {
        task::Project::clear().context("Failed to clear currently focused project")?;
    }

    Ok(())
}