use std::process::Stdio;
use std::time::{Duration, Instant};

use anyhow::{Context, Result};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{Child, Command};
use tokio::sync::mpsc;
use tracing::{error, info};

use crate::config::BranchConfig;
use crate::discord;
use crate::git;
use crate::tui::OutputMsg;

pub async fn watch(config: BranchConfig, tx: mpsc::UnboundedSender<OutputMsg>, shell_id: usize) {
    let label = match &config.name {
        Some(n) => format!("{n} ({branch})", branch = config.branch),
        None => format!("{branch} ({dir})", branch = config.branch, dir = config.directory.file_name().unwrap_or_default().to_string_lossy()),
    };

    let _ = tx.send(OutputMsg::Register { shell_id, name: label.clone() });

    let mut state = BranchState {
        config,
        label,
        deploy_child: None,
        last_sha: None,
        tx,
        shell_id,
    };

    if let Err(e) = state.initial_deploy().await {
        error!(branch = %state.label, "initial deploy failed: {e}");
        state.emit_line(format!("── initial deploy failed: {e} ──"));
        return;
    }

    loop {
        tokio::time::sleep(state.config.poll_rate).await;

        if let Err(e) = state.poll().await {
            error!(branch = %state.label, "poll error: {e}");
        }
    }
}

struct BranchState {
    config: BranchConfig,
    label: String,
    deploy_child: Option<Child>,
    last_sha: Option<String>,
    tx: mpsc::UnboundedSender<OutputMsg>,
    shell_id: usize,
}

impl BranchState {
    fn emit_line(&self, content: impl Into<Vec<u8>>) {
        let _ = self.tx.send(OutputMsg::Line {
            shell_id: self.shell_id,
            content: content.into(),
        });
    }

    async fn initial_deploy(&mut self) -> Result<()> {
        let sha = self.get_remote_sha().await?;
        info!(branch = %self.label, sha = &sha[..8], "initial fetch");

        git::fetch_branch(&self.config.directory, &self.config.branch).await?;
        git::reset_to_remote(&self.config.directory, &self.config.branch).await?;

        let subject = git::commit_subject(&self.config.directory, &sha).await
            .unwrap_or_default();
        self.emit_line(format!("── {} — {} ──", &sha[..8], subject));

        self.notify_build_started(None, &sha, &subject).await;

        let start = Instant::now();
        let build_result = self.build().await;
        let elapsed = start.elapsed();

        if let Err(e) = build_result {
            info!(branch = %self.label, "build failed in {:.1}s", elapsed.as_secs_f64());
            self.notify_build_failed(&sha, &subject, elapsed).await;
            return Err(e);
        }

        info!(branch = %self.label, "build succeeded in {:.1}s", elapsed.as_secs_f64());
        self.notify_build_succeeded(&sha, &subject, elapsed).await;
        self.start_deploy()?;
        self.last_sha = Some(sha);
        Ok(())
    }

    async fn poll(&mut self) -> Result<()> {
        let sha = self.get_remote_sha().await?;

        if self.last_sha.as_deref() == Some(&sha) {
            return Ok(());
        }

        info!(branch = %self.label, sha = &sha[..8], "change detected, updating");
        let old_sha = self.last_sha.replace(sha.clone());

        git::fetch_branch(&self.config.directory, &self.config.branch).await?;
        git::reset_to_remote(&self.config.directory, &self.config.branch).await?;

        let subject = git::commit_subject(&self.config.directory, &sha).await
            .unwrap_or_default();
        self.emit_line(format!("── {} — {} ──", &sha[..8], subject));

        self.notify_build_started(old_sha.as_deref(), &sha, &subject).await;

        let start = Instant::now();
        let build_result = self.build().await;
        let elapsed = start.elapsed();

        if let Err(e) = build_result {
            info!(branch = %self.label, "build failed in {:.1}s", elapsed.as_secs_f64());
            self.notify_build_failed(&sha, &subject, elapsed).await;
            return Err(e);
        }

        info!(branch = %self.label, "build succeeded in {:.1}s", elapsed.as_secs_f64());
        self.notify_build_succeeded(&sha, &subject, elapsed).await;
        self.kill_deploy();
        self.start_deploy()?;
        Ok(())
    }

    async fn get_remote_sha(&self) -> Result<String> {
        let refs = git::ls_remote(&self.config.directory).await?;
        let by_branch = git::refs_by_branch(&refs);
        by_branch
            .get(self.config.branch.as_str())
            .map(|s| s.to_string())
            .ok_or_else(|| {
                anyhow::anyhow!(
                    "branch '{}' not found on remote",
                    self.config.branch
                )
            })
    }

    async fn build(&self) -> Result<()> {
        info!(branch = %self.label, cmd = %self.config.build, "building");
        self.emit_line(format!("── building: {} ──", self.config.build));

        let mut child = Command::new("sh")
            .args(["-c", &self.config.build])
            .current_dir(&self.config.directory)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .context("failed to spawn build command")?;

        let stdout = child.stdout.take().unwrap();
        let stderr = child.stderr.take().unwrap();

        let tx1 = self.tx.clone();
        let id = self.shell_id;
        let stdout_task = tokio::spawn(async move {
            let mut reader = BufReader::new(stdout).lines();
            while let Ok(Some(line)) = reader.next_line().await {
                let _ = tx1.send(OutputMsg::Line { shell_id: id, content: line.into_bytes() });
            }
        });

        let tx2 = self.tx.clone();
        let id = self.shell_id;
        let stderr_task = tokio::spawn(async move {
            let mut reader = BufReader::new(stderr).lines();
            while let Ok(Some(line)) = reader.next_line().await {
                let _ = tx2.send(OutputMsg::Line { shell_id: id, content: line.into_bytes() });
            }
        });

        let status = child.wait().await.context("build process error")?;
        let _ = stdout_task.await;
        let _ = stderr_task.await;

        if !status.success() {
            self.emit_line(format!("── build failed ({}) ──", status));
            anyhow::bail!("build failed with {}", status);
        }

        self.emit_line("── build succeeded ──".to_string());
        Ok(())
    }

    fn start_deploy(&mut self) -> Result<()> {
        info!(branch = %self.label, cmd = %self.config.deploy, "starting deploy");
        self.emit_line(format!("── deploy: {} ──", self.config.deploy));

        let mut child = Command::new("sh")
            .args(["-c", &self.config.deploy])
            .current_dir(&self.config.directory)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .kill_on_drop(true)
            .spawn()
            .context("failed to spawn deploy command")?;

        let stdout = child.stdout.take().unwrap();
        let stderr = child.stderr.take().unwrap();

        let tx1 = self.tx.clone();
        let id = self.shell_id;
        tokio::spawn(async move {
            let mut reader = BufReader::new(stdout).lines();
            while let Ok(Some(line)) = reader.next_line().await {
                let _ = tx1.send(OutputMsg::Line { shell_id: id, content: line.into_bytes() });
            }
        });

        let tx2 = self.tx.clone();
        let id = self.shell_id;
        tokio::spawn(async move {
            let mut reader = BufReader::new(stderr).lines();
            while let Ok(Some(line)) = reader.next_line().await {
                let _ = tx2.send(OutputMsg::Line { shell_id: id, content: line.into_bytes() });
            }
        });

        self.deploy_child = Some(child);
        Ok(())
    }

    fn kill_deploy(&mut self) {
        if let Some(ref mut child) = self.deploy_child {
            info!(branch = %self.label, "stopping previous deploy");
            let _ = child.start_kill();
        }
        self.deploy_child = None;
    }

    async fn notify_build_started(&self, old_sha: Option<&str>, new_sha: &str, subject: &str) {
        if let Some(url) = &self.config.discord {
            discord::build_started(
                url,
                &self.config.branch,
                self.config.name.as_deref(),
                old_sha,
                new_sha,
                subject,
            )
            .await;
        }
    }

    async fn notify_build_succeeded(&self, sha: &str, subject: &str, elapsed: Duration) {
        if let Some(url) = &self.config.discord {
            discord::build_succeeded(
                url,
                &self.config.branch,
                self.config.name.as_deref(),
                sha,
                subject,
                elapsed,
            )
            .await;
        }
    }

    async fn notify_build_failed(&self, sha: &str, subject: &str, elapsed: Duration) {
        if let Some(url) = &self.config.discord {
            discord::build_failed(
                url,
                &self.config.branch,
                self.config.name.as_deref(),
                sha,
                subject,
                elapsed,
            )
            .await;
        }
    }
}

impl Drop for BranchState {
    fn drop(&mut self) {
        self.kill_deploy();
    }
}
