use std::path::Path;

use anyhow::{anyhow, Context, Result};
use sea_orm::DatabaseConnection;
use tempfile::TempDir;
use tracing::{error, info, warn};

use crate::config::WorkflowConfig;
use crate::entities::repo::Model as Repo;
use crate::entities::task::Model as Task;
use robson_connector_bitbucket::{render_pr_body, BitbucketPrClient};
use robson_connector_git as git;

use crate::scan_for_misplaced_tests;
use robson_connector_claude::{
    compose_docker_impl_prompt, compose_impl_prompt, compose_test_correction_prompt,
    implement_in_repo,
};
use robson_connector_docker::{generate_project_name, parse_docker_compose, DockerSession};

use super::context::TaskExecutionContext;

pub fn default_pr_template() -> String {
    r#"## {summary}

**Jira:** [{key}]({jira_url})

### Description
{description}

### Checklist
- [ ] Tests added/updated
- [ ] Documentation updated
"#
    .to_string()
}

/// Returns `(owner_or_workspace, repository_name)` for building clone URLs and PR API paths.
fn parse_owner_repo_from_slug(slug: &str) -> Result<(&str, &str)> {
    let slug = slug.trim();
    if slug.starts_with("git@") {
        let path = slug
            .split_once(':')
            .map(|x| x.1)
            .ok_or_else(|| anyhow!("invalid git SSH URL (missing ':' path)"))?;
        return two_path_segments(path);
    }
    if slug.starts_with("ssh://") {
        let rest = slug.strip_prefix("ssh://").unwrap_or(slug);
        let after_host = rest
            .find('/')
            .map(|i| &rest[i + 1..])
            .ok_or_else(|| anyhow!("invalid ssh:// git URL (missing path after host)"))?;
        return two_path_segments(after_host);
    }
    if slug.starts_with("https://") || slug.starts_with("http://") {
        let without_scheme = slug
            .find("://")
            .map(|i| &slug[i + 3..])
            .ok_or_else(|| anyhow!("invalid HTTP(S) URL"))?;
        let parts: Vec<&str> = without_scheme
            .split('/')
            .filter(|s| !s.is_empty())
            .collect();
        if parts.len() >= 3 {
            return Ok((parts[1], parts[2].trim_end_matches(".git")));
        }
        anyhow::bail!(
            "HTTP(S) repo URL must include owner and repo (e.g. https://github.com/org/repo)"
        );
    }
    if slug.starts_with("git://") {
        let rest = slug.strip_prefix("git://").unwrap_or(slug);
        let parts: Vec<&str> = rest.split('/').filter(|s| !s.is_empty()).collect();
        if parts.len() >= 3 {
            return Ok((parts[1], parts[2].trim_end_matches(".git")));
        }
        anyhow::bail!("git:// URL must be like git://host/owner/repo");
    }
    if !slug.contains("://") && !slug.contains(':') {
        if let Some((a, b)) = slug.split_once('/') {
            if !a.is_empty() && !b.is_empty() {
                return Ok((a, b.trim_end_matches(".git")));
            }
        }
    }
    anyhow::bail!(
        "repo slug must be a remote URL (git@..., https://..., git://..., ssh://...) \
         or shorthand `owner/repo`; got {:?}",
        slug
    )
}

fn two_path_segments(path: &str) -> Result<(&str, &str)> {
    let mut parts = path.split('/').filter(|p| !p.is_empty());
    let owner = parts
        .next()
        .ok_or_else(|| anyhow!("missing owner/workspace in path"))?;
    let repo = parts
        .next()
        .ok_or_else(|| anyhow!("missing repository name in path"))?;
    Ok((
        owner.trim_end_matches(".git"),
        repo.trim_end_matches(".git"),
    ))
}

fn repo_slug_for_pr_api(slug: &str) -> Result<String> {
    let s = slug.trim();
    if s.starts_with("git@")
        || s.starts_with("https://")
        || s.starts_with("http://")
        || s.starts_with("git://")
        || s.starts_with("ssh://")
    {
        return Ok(git::extract_slug_from_url(s).to_string());
    }
    let (_w, r) = parse_owner_repo_from_slug(s)?;
    Ok(r.to_string())
}

pub fn build_clone_url_for_repo(_config: &WorkflowConfig, repo: &Repo) -> Result<String> {
    let s = repo.slug.trim();
    if s.starts_with("git@")
        || s.starts_with("https://")
        || s.starts_with("http://")
        || s.starts_with("git://")
        || s.starts_with("ssh://")
    {
        return Ok(repo.slug.clone());
    }
    let (workspace, name) = parse_owner_repo_from_slug(s)?;
    Ok(git::build_ssh_clone_url(&repo.provider, workspace, name))
}

pub fn clone_repository(
    clone_url: &str,
    repo: &Repo,
    dest: &Path,
    ssh_key_path: Option<&str>,
) -> Result<()> {
    info!("Cloning repo {}...", repo.slug);
    git::clone_repo(clone_url, &repo.target_branch, dest, ssh_key_path)
}

pub fn create_feature_branch(repo_dir: &Path, branch_name: &str) -> Result<()> {
    git::create_local_branch(repo_dir, branch_name)
}

pub async fn run_claude_implementation(
    task: &Task,
    repo: &Repo,
    ctx: &TaskExecutionContext,
    repo_dir: &Path,
    dev: bool,
    claude_bin: Option<&str>,
) -> Result<()> {
    let impl_prompt = compose_impl_prompt(
        &task.jira_key,
        &ctx.summary,
        &ctx.description_text,
        &[],
        repo.developer_focus.as_deref(),
        repo.custom_prompt.as_deref(),
    );

    info!(
        "[{}][{}] Running Claude Code implementation (direct)...",
        task.jira_key, repo.slug
    );
    let impl_result = implement_in_repo(&impl_prompt, repo_dir, dev, claude_bin).await?;

    if !impl_result.success {
        anyhow::bail!(
            "Claude implementation failed (exit {}): {}",
            impl_result.exit_code,
            impl_result.stderr
        );
    }

    Ok(())
}

pub fn push_branch_to_remote(
    repo_dir: &Path,
    branch_name: &str,
    ssh_key_path: Option<&str>,
) -> Result<()> {
    git::push_branch(repo_dir, branch_name, ssh_key_path)
}

pub async fn create_pull_request(
    _config: &WorkflowConfig,
    task: &Task,
    ctx: &TaskExecutionContext,
    repo: &Repo,
    branch_name: &str,
    jira_url: &str,
) -> Result<String> {
    let template = default_pr_template();
    let pr_body = render_pr_body(&template, &task.jira_key, &ctx.summary, "", jira_url);

    let (workspace, _) = parse_owner_repo_from_slug(&repo.slug).with_context(|| {
        format!(
            "Could not parse Bitbucket workspace from repo slug {:?}",
            repo.slug
        )
    })?;
    let repo_slug = repo_slug_for_pr_api(&repo.slug)?;

    BitbucketPrClient::new(&repo.token)?
        .create_pr_in(
            workspace,
            &repo_slug,
            &repo.target_branch,
            branch_name,
            &ctx.summary,
            &pr_body,
        )
        .await
}

#[allow(clippy::too_many_arguments)]
pub async fn run_implementation(
    config: &WorkflowConfig,
    db: &DatabaseConnection,
    task: &Task,
    repo: &Repo,
    ctx: &TaskExecutionContext,
    temp_dir: TempDir,
    no_push: bool,
    dev: bool,
) -> Result<(TempDir, String, bool)> {
    git::log_branch_reset(&repo.target_branch);
    let branch_name = git::make_branch_name(&task.jira_key, &ctx.summary);
    info!("[{}][{}] Branch: {}", task.jira_key, repo.slug, branch_name);

    match create_feature_branch(temp_dir.path(), &branch_name) {
        Ok(()) => info!(
            "[{}][{}] Feature branch '{}' created.",
            task.jira_key, repo.slug, branch_name
        ),
        Err(e) => {
            error!(
                "[{}][{}] Feature branch creation failed: {:#}",
                task.jira_key, repo.slug, e
            );
            return Err(e);
        }
    }

    let compose_info = if !dev && temp_dir.path().join("docker-compose.yml").exists() {
        match parse_docker_compose(temp_dir.path()) {
            Ok(info) => {
                info!(
                    "[{}][{}] docker-compose.yml found — app image: {}, support services: {:?}",
                    task.jira_key, repo.slug, info.app_image, info.support_services
                );
                Some(info)
            }
            Err(e) => {
                warn!(
                    "[{}][{}] Could not parse docker-compose.yml: {:#} — falling back to direct Claude execution.",
                    task.jira_key, repo.slug, e
                );
                None
            }
        }
    } else {
        None
    };

    let mut session = if compose_info.is_some() {
        let pname = generate_project_name(task.id.into());
        match DockerSession::new(&config.container, pname) {
            Ok(s) => Some(s),
            Err(e) => {
                warn!(
                    "[{}][{}] Failed to connect to Docker: {:#} — falling back to direct execution.",
                    task.jira_key, repo.slug, e
                );
                None
            }
        }
    } else {
        None
    };

    if let (Some(ref mut sess), Some(ref info)) = (&mut session, &compose_info) {
        info!(
            "[{}][{}] Starting support services...",
            task.jira_key, repo.slug
        );
        if let Err(e) = sess.start_support_services(info, temp_dir.path()).await {
            error!(
                "[{}][{}] Failed to start support services: {:#}",
                task.jira_key, repo.slug, e
            );
            let _ = sess.teardown().await;
            return Err(e);
        }
    }

    let claude_result = match (&mut session, &compose_info) {
        (Some(ref mut sess), Some(ci)) => {
            let prompt = compose_docker_impl_prompt(
                &task.jira_key,
                &ctx.summary,
                &ctx.description_text,
                &[],
                repo.developer_focus.as_deref(),
                repo.custom_prompt.as_deref(),
            );
            info!(
                "[{}][{}] Running Claude Code in Docker container...",
                task.jira_key, repo.slug
            );
            match sess
                .run_claude_in_container(
                    &ci.app_image,
                    temp_dir.path(),
                    &prompt,
                    &config.container,
                    config.orchestrator.claude_bin.as_deref(),
                )
                .await
            {
                Ok(output) if output.exit_code == 0 => Ok(()),
                Ok(output) => Err(anyhow::anyhow!(
                    "Claude container exited with non-zero status (exit code: {})",
                    output.exit_code
                )),
                Err(e) => Err(e),
            }
        }
        _ => {
            run_claude_implementation(
                task,
                repo,
                ctx,
                temp_dir.path(),
                dev,
                config.orchestrator.claude_bin.as_deref(),
            )
            .await
        }
    };

    if let Err(e) = claude_result {
        error!(
            "[{}][{}] Claude implementation failed: {:#}",
            task.jira_key, repo.slug, e
        );
        if let Some(ref mut sess) = session {
            let _ = sess.teardown().await;
        }
        return Err(e);
    }

    info!(
        "[{}][{}] Claude implementation complete.",
        task.jira_key, repo.slug
    );

    let violations = scan_for_misplaced_tests(temp_dir.path());
    if !violations.is_empty() {
        warn!(
            "[{}][{}] Found {} file(s) with tests outside tests/ — running correction pass.",
            task.jira_key,
            repo.slug,
            violations.len()
        );

        let correction_prompt = compose_test_correction_prompt();
        let correction_result = match (&mut session, &compose_info) {
            (Some(ref mut sess), Some(ci)) => {
                match sess
                    .run_claude_in_container(
                        &ci.app_image,
                        temp_dir.path(),
                        &correction_prompt,
                        &config.container,
                        config.orchestrator.claude_bin.as_deref(),
                    )
                    .await
                {
                    Ok(output) if output.exit_code == 0 => Ok(()),
                    Ok(output) => Err(anyhow::anyhow!(
                        "Correction container exited with non-zero status ({})",
                        output.exit_code
                    )),
                    Err(e) => Err(e),
                }
            }
            _ => implement_in_repo(
                &correction_prompt,
                temp_dir.path(),
                dev,
                config.orchestrator.claude_bin.as_deref(),
            )
            .await
            .and_then(|r| {
                if r.success {
                    Ok(())
                } else {
                    Err(anyhow::anyhow!(
                        "Correction pass failed (exit {})",
                        r.exit_code
                    ))
                }
            }),
        };

        if let Err(e) = correction_result {
            warn!(
                "[{}][{}] Correction pass failed: {:#}",
                task.jira_key, repo.slug, e
            );
            if let Some(ref mut sess) = session {
                let _ = sess.teardown().await;
            }
            return Ok((temp_dir, branch_name, false));
        }

        let remaining = scan_for_misplaced_tests(temp_dir.path());
        if !remaining.is_empty() {
            warn!(
                "[{}][{}] {} file(s) still have misplaced tests after correction — marking unfinished.",
                task.jira_key, repo.slug, remaining.len()
            );
            if let Some(ref mut sess) = session {
                let _ = sess.teardown().await;
            }
            return Ok((temp_dir, branch_name, false));
        }

        info!(
            "[{}][{}] Correction pass successful — all tests moved to tests/.",
            task.jira_key, repo.slug
        );
    }

    info!("[{}][{}] Committing changes...", task.jira_key, repo.slug);
    match git::commit_changes(
        temp_dir.path(),
        &task.jira_key,
        &config.orchestrator.git_name,
        &config.orchestrator.git_email,
    ) {
        Ok(()) => info!(
            "[{}][{}] Changes committed successfully.",
            task.jira_key, repo.slug
        ),
        Err(e) => {
            error!("[{}][{}] Commit failed: {:#}", task.jira_key, repo.slug, e);
            if let Some(ref mut sess) = session {
                let _ = sess.teardown().await;
            }
            return Err(e);
        }
    }

    if let Some(ref mut sess) = session {
        let _ = sess.teardown().await;
    }

    let pr_url = if no_push {
        let template = default_pr_template();
        let pr_body = render_pr_body(&template, &task.jira_key, &ctx.summary, "", &ctx.jira_url);
        println!(
            "[{}][{}] --no-push: skipping push and PR creation.\n  Branch:     {}\n  Local repo: {}\n  PR body:\n---\n{}\n---",
            task.jira_key, repo.slug, branch_name, temp_dir.path().display(), pr_body,
        );
        String::new()
    } else {
        let _ = git::log_commit_info(temp_dir.path());
        let _ = git::log_commit_diff(temp_dir.path());

        let ssh_key = config.orchestrator.ssh_key_path.as_deref();
        info!("[{}][{}] Pushing branch...", task.jira_key, repo.slug);
        match push_branch_to_remote(temp_dir.path(), &branch_name, ssh_key) {
            Ok(()) => info!(
                "[{}][{}] Branch '{}' pushed successfully.",
                task.jira_key, repo.slug, branch_name
            ),
            Err(e) => {
                error!("[{}][{}] Push failed: {:#}", task.jira_key, repo.slug, e);
                return Err(e);
            }
        }

        info!("[{}][{}] Creating PR...", task.jira_key, repo.slug);
        match create_pull_request(config, task, ctx, repo, &branch_name, &ctx.jira_url).await {
            Ok(url) => {
                info!("[{}][{}] PR created: {}", task.jira_key, repo.slug, url);
                url
            }
            Err(e) => {
                error!(
                    "[{}][{}] PR creation failed: {:#}",
                    task.jira_key, repo.slug, e
                );
                return Err(e);
            }
        }
    };

    Task::set_done(db, &task.jira_key, &branch_name, &pr_url).await?;

    Ok((temp_dir, branch_name, true))
}
