// Webhook-templates: forhåndsdefinerte mappinger for kjente tjenester (oppgave 29.6)
//
// Hver template vet hvordan den skal ekstrahere title, content og metadata
// fra en innkommende JSON-payload. Templates er hardkodet — de dekker kjente
// tjenester (GitHub, Slack, CI/CD). Webhook-noder kan ha en template_id i
// metadata som styrer hvilken template som brukes ved mottak.
//
// Hvis ingen template er satt, brukes den generiske ekstraksjonen fra
// webhook.rs (title, content/body-felt).

use serde::Serialize;

/// Resultat av å anvende en template på en payload.
pub struct TemplateResult {
    pub title: String,
    pub content: String,
    /// Ekstra metadata-felt som legges til node-metadata (utover source/webhook_id/payload).
    pub extra_metadata: serde_json::Value,
}

/// En forhåndsdefinert template-definisjon.
#[derive(Serialize, Clone)]
pub struct TemplateInfo {
    pub id: &'static str,
    pub name: &'static str,
    pub description: &'static str,
    pub service: &'static str,
}

/// Alle tilgjengelige templates.
pub fn list_templates() -> Vec<TemplateInfo> {
    vec![
        TemplateInfo {
            id: "github-push",
            name: "GitHub Push",
            description: "Commits pushet til et repository",
            service: "GitHub",
        },
        TemplateInfo {
            id: "github-issues",
            name: "GitHub Issues",
            description: "Issue opprettet, endret eller lukket",
            service: "GitHub",
        },
        TemplateInfo {
            id: "github-pull-request",
            name: "GitHub Pull Request",
            description: "PR opprettet, merget eller lukket",
            service: "GitHub",
        },
        TemplateInfo {
            id: "slack-message",
            name: "Slack-melding",
            description: "Melding sendt i en Slack-kanal",
            service: "Slack",
        },
        TemplateInfo {
            id: "ci-build",
            name: "CI/CD Build",
            description: "Byggstatus fra CI/CD-pipeline (GitLab CI, GitHub Actions, Jenkins)",
            service: "CI/CD",
        },
    ]
}

/// Finn en template basert på id.
pub fn get_template(id: &str) -> Option<TemplateInfo> {
    list_templates().into_iter().find(|t| t.id == id)
}

/// Anvend en template på en payload. Returnerer None hvis template_id er ukjent.
pub fn apply_template(template_id: &str, payload: &serde_json::Value) -> Option<TemplateResult> {
    match template_id {
        "github-push" => Some(apply_github_push(payload)),
        "github-issues" => Some(apply_github_issues(payload)),
        "github-pull-request" => Some(apply_github_pull_request(payload)),
        "slack-message" => Some(apply_slack_message(payload)),
        "ci-build" => Some(apply_ci_build(payload)),
        _ => None,
    }
}

// =============================================================================
// GitHub Push
// =============================================================================

fn apply_github_push(p: &serde_json::Value) -> TemplateResult {
    let repo = json_str(p, &["repository", "full_name"])
        .or_else(|| json_str(p, &["repository", "name"]))
        .unwrap_or_default();
    let git_ref = json_str(p, &["ref"]).unwrap_or_default();
    // ref er typisk "refs/heads/main" — vis bare branch-navnet
    let branch = git_ref.strip_prefix("refs/heads/").unwrap_or(&git_ref);
    let pusher = json_str(p, &["pusher", "name"])
        .or_else(|| json_str(p, &["sender", "login"]))
        .unwrap_or_default();

    // Samle commit-meldinger
    let commits = p.get("commits").and_then(|c| c.as_array());
    let commit_count = commits.map(|c| c.len()).unwrap_or(0);

    let title = if repo.is_empty() {
        format!("Push: {commit_count} commit(s)")
    } else {
        format!("[{repo}] Push til {branch} — {commit_count} commit(s)")
    };

    let content = if let Some(commits) = commits {
        commits
            .iter()
            .filter_map(|c| {
                let sha = c.get("id").and_then(|v| v.as_str()).unwrap_or("").get(..7).unwrap_or("");
                let msg = c.get("message").and_then(|v| v.as_str()).unwrap_or("");
                if msg.is_empty() { None } else { Some(format!("- `{sha}` {msg}")) }
            })
            .collect::<Vec<_>>()
            .join("\n")
    } else {
        String::new()
    };

    let extra = serde_json::json!({
        "template": "github-push",
        "repo": repo,
        "branch": branch,
        "pusher": pusher,
        "commit_count": commit_count,
    });

    TemplateResult { title, content, extra_metadata: extra }
}

// =============================================================================
// GitHub Issues
// =============================================================================

fn apply_github_issues(p: &serde_json::Value) -> TemplateResult {
    let action = json_str(p, &["action"]).unwrap_or_default();
    let repo = json_str(p, &["repository", "full_name"]).unwrap_or_default();
    let number = p.get("issue").and_then(|i| i.get("number")).and_then(|n| n.as_i64()).unwrap_or(0);
    let issue_title = json_str(p, &["issue", "title"]).unwrap_or_default();
    let body = json_str(p, &["issue", "body"]).unwrap_or_default();
    let user = json_str(p, &["issue", "user", "login"])
        .or_else(|| json_str(p, &["sender", "login"]))
        .unwrap_or_default();
    let state = json_str(p, &["issue", "state"]).unwrap_or_default();

    let labels: Vec<String> = p
        .get("issue")
        .and_then(|i| i.get("labels"))
        .and_then(|l| l.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|l| l.get("name").and_then(|n| n.as_str()).map(String::from))
                .collect()
        })
        .unwrap_or_default();

    let title = if repo.is_empty() {
        format!("Issue #{number}: {issue_title}")
    } else {
        format!("[{repo}] Issue #{number} {action}: {issue_title}")
    };

    let extra = serde_json::json!({
        "template": "github-issues",
        "action": action,
        "repo": repo,
        "issue_number": number,
        "user": user,
        "state": state,
        "labels": labels,
    });

    TemplateResult { title, content: body, extra_metadata: extra }
}

// =============================================================================
// GitHub Pull Request
// =============================================================================

fn apply_github_pull_request(p: &serde_json::Value) -> TemplateResult {
    let action = json_str(p, &["action"]).unwrap_or_default();
    let repo = json_str(p, &["repository", "full_name"]).unwrap_or_default();
    let number = p.get("pull_request").and_then(|pr| pr.get("number")).and_then(|n| n.as_i64()).unwrap_or(0);
    let pr_title = json_str(p, &["pull_request", "title"]).unwrap_or_default();
    let body = json_str(p, &["pull_request", "body"]).unwrap_or_default();
    let user = json_str(p, &["pull_request", "user", "login"])
        .or_else(|| json_str(p, &["sender", "login"]))
        .unwrap_or_default();
    let state = json_str(p, &["pull_request", "state"]).unwrap_or_default();
    let merged = p.get("pull_request").and_then(|pr| pr.get("merged")).and_then(|m| m.as_bool()).unwrap_or(false);
    let base = json_str(p, &["pull_request", "base", "ref"]).unwrap_or_default();
    let head = json_str(p, &["pull_request", "head", "ref"]).unwrap_or_default();

    let title = if repo.is_empty() {
        format!("PR #{number} {action}: {pr_title}")
    } else {
        format!("[{repo}] PR #{number} {action}: {pr_title}")
    };

    let extra = serde_json::json!({
        "template": "github-pull-request",
        "action": action,
        "repo": repo,
        "pr_number": number,
        "user": user,
        "state": state,
        "merged": merged,
        "base_branch": base,
        "head_branch": head,
    });

    TemplateResult { title, content: body, extra_metadata: extra }
}

// =============================================================================
// Slack Message
// =============================================================================

fn apply_slack_message(p: &serde_json::Value) -> TemplateResult {
    // Slack Event API sender event.type = "message" inne i en event-wrapper
    let event = p.get("event").unwrap_or(p);

    let channel = json_str(event, &["channel"])
        .or_else(|| json_str(p, &["channel_name"]))
        .or_else(|| json_str(p, &["channel"]))
        .unwrap_or_default();
    let user = json_str(event, &["user"])
        .or_else(|| json_str(event, &["username"]))
        .or_else(|| json_str(p, &["user_name"]))
        .unwrap_or_default();
    let text = json_str(event, &["text"])
        .or_else(|| json_str(p, &["text"]))
        .unwrap_or_default();
    let team = json_str(p, &["team_id"])
        .or_else(|| json_str(event, &["team"]))
        .unwrap_or_default();
    let ts = json_str(event, &["ts"])
        .or_else(|| json_str(p, &["message_ts"]))
        .unwrap_or_default();

    let title = if channel.is_empty() {
        format!("Slack: {user}")
    } else {
        format!("Slack #{channel} — {user}")
    };

    let extra = serde_json::json!({
        "template": "slack-message",
        "channel": channel,
        "user": user,
        "team": team,
        "ts": ts,
    });

    TemplateResult { title, content: text, extra_metadata: extra }
}

// =============================================================================
// CI/CD Build Status (generisk — dekker GitHub Actions, GitLab CI, Jenkins)
// =============================================================================

fn apply_ci_build(p: &serde_json::Value) -> TemplateResult {
    // Prøv flere vanlige CI-payload-formater

    // GitHub Actions: check_run / workflow_run
    let status = json_str(p, &["check_run", "conclusion"])
        .or_else(|| json_str(p, &["workflow_run", "conclusion"]))
        .or_else(|| json_str(p, &["build", "status"]))
        .or_else(|| json_str(p, &["object_attributes", "status"])) // GitLab
        .or_else(|| json_str(p, &["status"]))
        .unwrap_or_else(|| "unknown".to_string());

    let pipeline = json_str(p, &["workflow_run", "name"])
        .or_else(|| json_str(p, &["check_run", "name"]))
        .or_else(|| json_str(p, &["object_attributes", "name"])) // GitLab
        .or_else(|| json_str(p, &["build", "full_display_name"])) // Jenkins
        .or_else(|| json_str(p, &["name"]))
        .unwrap_or_else(|| "Build".to_string());

    let repo = json_str(p, &["repository", "full_name"])
        .or_else(|| json_str(p, &["project", "path_with_namespace"])) // GitLab
        .unwrap_or_default();

    let url = json_str(p, &["workflow_run", "html_url"])
        .or_else(|| json_str(p, &["check_run", "html_url"]))
        .or_else(|| json_str(p, &["build", "url"])) // Jenkins
        .or_else(|| json_str(p, &["object_attributes", "url"])) // GitLab
        .unwrap_or_default();

    let duration = p.get("workflow_run")
        .and_then(|wr| wr.get("run_started_at"))
        .and_then(|_| p.get("workflow_run").and_then(|wr| wr.get("updated_at")))
        .map(|_| "se payload".to_string())
        .or_else(|| json_str(p, &["object_attributes", "duration"]).map(|d| format!("{d}s")))
        .or_else(|| json_str(p, &["build", "duration"]))
        .unwrap_or_default();

    let branch = json_str(p, &["workflow_run", "head_branch"])
        .or_else(|| json_str(p, &["check_run", "check_suite", "head_branch"]))
        .or_else(|| json_str(p, &["object_attributes", "ref"])) // GitLab
        .or_else(|| json_str(p, &["ref"]))
        .unwrap_or_default();

    let status_emoji = match status.as_str() {
        "success" => "OK",
        "failure" | "failed" => "FEIL",
        "cancelled" | "canceled" => "AVBRUTT",
        "pending" | "running" | "in_progress" => "KJØRER",
        _ => &status,
    };

    let title = if repo.is_empty() {
        format!("Build {status_emoji}: {pipeline}")
    } else {
        format!("[{repo}] Build {status_emoji}: {pipeline}")
    };

    let mut content_parts: Vec<String> = Vec::new();
    if !branch.is_empty() {
        content_parts.push(format!("Branch: {branch}"));
    }
    if !duration.is_empty() {
        content_parts.push(format!("Varighet: {duration}"));
    }
    if !url.is_empty() {
        content_parts.push(format!("Lenke: {url}"));
    }
    let content = content_parts.join("\n");

    let extra = serde_json::json!({
        "template": "ci-build",
        "status": status,
        "pipeline": pipeline,
        "repo": repo,
        "branch": branch,
        "url": url,
    });

    TemplateResult { title, content, extra_metadata: extra }
}

// =============================================================================
// Hjelpefunksjon: traverser nestet JSON med nøkkel-path
// =============================================================================

fn json_str(value: &serde_json::Value, path: &[&str]) -> Option<String> {
    let mut current = value;
    for key in path {
        current = current.get(*key)?;
    }
    current.as_str().map(String::from)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_github_push_template() {
        let payload = serde_json::json!({
            "ref": "refs/heads/main",
            "repository": { "full_name": "vegard/synops", "name": "synops" },
            "pusher": { "name": "vegard" },
            "commits": [
                { "id": "abc1234567890", "message": "Fix bug" },
                { "id": "def4567890123", "message": "Add feature" }
            ]
        });
        let result = apply_template("github-push", &payload).unwrap();
        assert!(result.title.contains("vegard/synops"));
        assert!(result.title.contains("main"));
        assert!(result.title.contains("2 commit(s)"));
        assert!(result.content.contains("Fix bug"));
        assert!(result.content.contains("Add feature"));
    }

    #[test]
    fn test_github_issues_template() {
        let payload = serde_json::json!({
            "action": "opened",
            "repository": { "full_name": "vegard/synops" },
            "issue": {
                "number": 42,
                "title": "Something is broken",
                "body": "Details about the bug",
                "user": { "login": "vegard" },
                "state": "open",
                "labels": [{ "name": "bug" }]
            }
        });
        let result = apply_template("github-issues", &payload).unwrap();
        assert!(result.title.contains("#42"));
        assert!(result.title.contains("opened"));
        assert_eq!(result.content, "Details about the bug");
    }

    #[test]
    fn test_slack_message_template() {
        let payload = serde_json::json!({
            "event": {
                "type": "message",
                "channel": "general",
                "user": "U123",
                "text": "Hello world",
                "ts": "1234567890.123456"
            },
            "team_id": "T123"
        });
        let result = apply_template("slack-message", &payload).unwrap();
        assert!(result.title.contains("general"));
        assert_eq!(result.content, "Hello world");
    }

    #[test]
    fn test_ci_build_template() {
        let payload = serde_json::json!({
            "workflow_run": {
                "name": "CI",
                "conclusion": "success",
                "head_branch": "main",
                "html_url": "https://github.com/vegard/synops/actions/runs/123"
            },
            "repository": { "full_name": "vegard/synops" }
        });
        let result = apply_template("ci-build", &payload).unwrap();
        assert!(result.title.contains("OK"));
        assert!(result.title.contains("CI"));
        assert!(result.content.contains("main"));
    }

    #[test]
    fn test_unknown_template_returns_none() {
        let payload = serde_json::json!({});
        assert!(apply_template("unknown", &payload).is_none());
    }

    #[test]
    fn test_list_templates() {
        let templates = list_templates();
        assert_eq!(templates.len(), 5);
        assert!(templates.iter().any(|t| t.id == "github-push"));
        assert!(templates.iter().any(|t| t.id == "slack-message"));
        assert!(templates.iter().any(|t| t.id == "ci-build"));
    }
}
