use std::sync::Arc;

use anyhow::Result;
use robson_connector_bitbucket::BitbucketPrClient;
use sea_orm::DatabaseConnection;
use tracing::{debug, error, info, warn};

use crate::config::{WorkflowConfig, WorkflowState};
use crate::entities::repo::Model as Repo;
use crate::entities::task::Model as Task;
use crate::entities::task_execution::Model as TaskExecution;
use crate::orchestrator::run_task::{run_single_task, RunTaskFlags};

// ── /task list ────────────────────────────────────────────────────────────────

pub async fn execute_task_list(db: &DatabaseConnection) -> Result<String> {
    let execs = TaskExecution::find_latest_per_task(db).await?;

    let headers = ["KEY", "SUMMARY", "STATUS", "REPO_URL", "PR_URL"];
    let mut rows: Vec<[String; 5]> = Vec::with_capacity(execs.len());
    for ex in execs {
        let Some(t) = Task::find_by_id(db, ex.task_id).await? else {
            continue;
        };
        let repo = Repo::find_by_id(db, ex.repo_id).await?;
        let slug = repo.map(|r| r.slug).unwrap_or_default();
        rows.push([
            t.jira_key.clone(),
            ex.summary.clone(),
            ex.status.to_string(),
            truncate(&slug, 40),
            truncate(ex.pr_url.as_deref().unwrap_or(""), 40),
        ]);
    }

    Ok(format_table(&headers, &rows))
}

// ── /repo list ────────────────────────────────────────────────────────────────

pub async fn execute_repo_list(db: &DatabaseConnection) -> Result<String> {
    let repos = Repo::find_all(db).await?;

    let headers = ["SLUG", "PROVIDER", "TARGET_BRANCH"];
    let rows: Vec<[String; 3]> = repos
        .iter()
        .map(|r| {
            [
                truncate(&r.slug, 40),
                r.provider.clone(),
                r.target_branch.clone(),
            ]
        })
        .collect();

    Ok(format_table(&headers, &rows))
}

// ── /bitbucket list ───────────────────────────────────────────────────────────

pub async fn execute_bitbucket_repo_list(
    config: &WorkflowConfig,
    workspace_override: Option<String>,
    db: &DatabaseConnection,
) -> Result<String> {
    let bb = config.bitbucket.as_ref().ok_or_else(|| {
        anyhow::anyhow!(
            "Configure [bitbucket] in robson.toml (workspace, token) to list remote repositories."
        )
    })?;
    let token = bb.token.as_deref().ok_or_else(|| {
        anyhow::anyhow!("Set bitbucket.token in robson.toml to call the Bitbucket REST API.")
    })?;
    let ws = workspace_override
        .as_deref()
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .unwrap_or(bb.workspace.as_str());

    let client = match bb
        .api_base
        .as_deref()
        .map(str::trim)
        .filter(|s| !s.is_empty())
    {
        Some(base) => BitbucketPrClient::new_with_base(token, base)?,
        None => BitbucketPrClient::new(token)?,
    };
    let default_branch = bb.base_branch.as_deref();
    let repos = client
        .list_repositories_in_workspace(ws, None, default_branch)
        .await?;

    const DISPLAY_CAP: usize = 200;
    let total = repos.len();
    if total == 0 {
        return Ok(format!(
            "\n_Bitbucket workspace `{}` — no repositories found (or no access)._",
            ws
        ));
    }

    for info in &repos {
        Repo::upsert_registered_repo(
            db,
            &info.git_url,
            &info.provider,
            &info.target_branch,
            token,
        )
        .await?;
    }

    let headers = ["SLUG", "PROVIDER", "TARGET_BRANCH"];
    let rows: Vec<[String; 3]> = repos
        .iter()
        .take(DISPLAY_CAP)
        .map(|r| {
            [
                truncate(&r.git_url, 56),
                truncate(&r.provider, 16),
                truncate(&r.target_branch, 24),
            ]
        })
        .collect();

    let mut out = format!(
        "\n*Bitbucket workspace `{}`* — {} repositories (synced to database)",
        ws, total
    );
    if total > DISPLAY_CAP {
        out.push_str(&format!(
            "\n_Showing first {} of {} (all fetched rows were written to the DB). Pass `workspace=<slug>` to query another workspace._",
            DISPLAY_CAP, total
        ));
    }
    out.push_str(&format_table(&headers, &rows));
    Ok(out)
}

// ── /run-task ─────────────────────────────────────────────────────────────────

pub async fn execute_run_task(
    key: String,
    repo_url: Option<String>,
    state: Arc<WorkflowState>,
) -> Result<String> {
    debug!(jira_key = %key, repo_url = ?repo_url, "execute_run_task called");

    if key.is_empty() {
        return Ok(":x: Missing required field: *key*\n\nUsage: `/run-task key=<JIRA-KEY> [repo_url=<git-url>]`".to_string());
    }

    let task = Task::find_by_key(&state.workflow_db, &key).await?;
    if task.is_none() {
        warn!(jira_key = %key, "task not found");
        return Ok(format!(":x: Task *{}* not found.", key));
    }

    let flags = RunTaskFlags {
        force: false,
        no_notify: false,
        no_push: false,
        keep_tmp: false,
        dev: false,
        repo_url,
    };
    match run_single_task(state, &key, flags).await {
        Ok(()) => {
            info!(jira_key = %key, "run-task complete");
            Ok(format!(
                ":white_check_mark: Task *{}* pipeline complete.",
                key
            ))
        }
        Err(e) => {
            error!(jira_key = %key, error = %e, "run-task failed");
            Ok(format!(":x: Task *{}* pipeline failed: {:#}", key, e))
        }
    }
}

// ── Shared formatting helpers ─────────────────────────────────────────────────

/// Truncate a string to `max` Unicode characters, appending `…` if truncated.
fn truncate(s: &str, max: usize) -> String {
    let chars: Vec<char> = s.chars().collect();
    if chars.len() <= max {
        s.to_string()
    } else {
        let mut t: String = chars[..max - 1].iter().collect();
        t.push('…');
        t
    }
}

/// Render a plain-text ASCII table with a leading blank line.
/// `N` is the number of columns.
fn format_table<const N: usize>(headers: &[&str; N], rows: &[[String; N]]) -> String {
    // Compute column widths = max(header width, max row width)
    let mut widths = [0usize; N];
    for (i, h) in headers.iter().enumerate() {
        widths[i] = h.chars().count();
    }
    for row in rows {
        for (i, cell) in row.iter().enumerate() {
            let w = cell.chars().count();
            if w > widths[i] {
                widths[i] = w;
            }
        }
    }

    let separator: String = widths
        .iter()
        .map(|&w| "-".repeat(w))
        .collect::<Vec<_>>()
        .join("-+-");

    let fmt_row = |cells: &[String; N]| -> String {
        cells
            .iter()
            .enumerate()
            .map(|(i, c)| {
                let pad = widths[i].saturating_sub(c.chars().count());
                format!("{}{}", c, " ".repeat(pad))
            })
            .collect::<Vec<_>>()
            .join(" | ")
    };

    let header_row: String = headers
        .iter()
        .enumerate()
        .map(|(i, h)| {
            let pad = widths[i].saturating_sub(h.chars().count());
            format!("{}{}", h, " ".repeat(pad))
        })
        .collect::<Vec<_>>()
        .join(" | ");

    let mut out = String::from("\n");
    out.push_str(&header_row);
    out.push('\n');
    out.push_str(&separator);
    for row in rows {
        out.push('\n');
        out.push_str(&fmt_row(row));
    }
    out
}
