use crate::feature::{
    FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureInstructionContribution,
    FeatureInstructionDeclaration, FeatureInstructionId, FeatureModule, ToolContribution,
    ToolDeclaration, ToolDefinition,
};
use crate::worker::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod};
use agen::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolOutput};
use async_trait::async_trait;
use manifest::MergeRequestFeatureConfig;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
use std::sync::Arc;

pub const FEATURE_ID: &str = "merge_request";
const FEATURE_NAME: &str = "Merge Request tools";
const FEATURE_DESCRIPTION: &str =
    "Operation-specific Merge Request workflow tools over Workspace authority.";
const FEATURE_INSTRUCTION_ID: &str = "merge_request.workflow";
pub const FEATURE_PROMPT_REF: &str = "common.merge_request";

fn workflow_instruction() -> FeatureInstructionDeclaration {
    FeatureInstructionDeclaration::new(
        FeatureInstructionId::builtin(FEATURE_INSTRUCTION_ID),
        FEATURE_PROMPT_REF,
        "Operation-specific Merge Request workflow guidance",
    )
    .expect("static Merge Request workflow instruction declaration is valid")
}

const ALL_KINDS: [Kind; 5] = [
    Kind::Show,
    Kind::Open,
    Kind::Review,
    Kind::Readiness,
    Kind::Complete,
];

#[derive(Clone, Copy)]
enum Kind {
    Show,
    Readiness,
    Open,
    Complete,
    Review,
}
#[derive(Clone)]
struct MergeRequestTool {
    client: Arc<dyn WorkspaceClient>,
    kind: Kind,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct ShowInput {
    ticket: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct OpenInput {
    ticket: String,
    repository_id: String,
    selector_from: String,
    selector_to: String,
    #[serde(default)]
    summary: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct CompleteInput {
    ticket: String,
    operation_id: String,
    approval_event_id: String,
    target_ref_before: String,
    target_ref_after: String,
    strategy: MergeStrategyInput,
    resolution: MergeResolutionInput,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum MergeStrategyInput {
    FastForward,
    Merge,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum MergeResolutionInput {
    None,
    Clean,
    ConflictsResolved,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct ReviewInput {
    decision: ReviewDecisionInput,
    #[serde(default)]
    body: String,
    #[serde(default)]
    findings: Vec<ReviewFindingInput>,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum ReviewDecisionInput {
    Approve,
    RequestChanges,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct ReviewFindingInput {
    severity: String,
    #[serde(default)]
    code: Option<String>,
    #[serde(default)]
    path: Option<String>,
    #[serde(default)]
    line: Option<u32>,
    body: String,
}
#[derive(Debug, Deserialize)]
struct TicketMergeRequestProjection {
    merge_request: Option<TicketMergeRequestReference>,
}

#[derive(Debug, Deserialize)]
struct TicketMergeRequestReference {
    merge_request_id: String,
}

impl Kind {
    fn enabled(self, config: MergeRequestFeatureConfig) -> bool {
        match self {
            Self::Show => config.show,
            Self::Open => config.open,
            Self::Review => config.review,
            Self::Readiness => config.readiness_check,
            Self::Complete => config.complete,
        }
    }

    fn name(self) -> &'static str {
        match self {
            Self::Show => "MergeRequestShow",
            Self::Readiness => "MergeRequestReadinessCheck",
            Self::Open => "MergeRequestOpen",
            Self::Complete => "MergeRequestComplete",
            Self::Review => "MergeRequestReview",
        }
    }
    fn schema(self) -> serde_json::Value {
        match self {
            Self::Show | Self::Readiness => json!(schemars::schema_for!(ShowInput)),
            Self::Open => json!(schemars::schema_for!(OpenInput)),
            Self::Complete => json!(schemars::schema_for!(CompleteInput)),
            Self::Review => json!(schemars::schema_for!(ReviewInput)),
        }
    }
}
#[async_trait]
impl Tool for MergeRequestTool {
    async fn execute(&self, input: &str, _: ToolExecutionContext) -> Result<ToolOutput, ToolError> {
        let ws = self.client.workspace_id().ok_or_else(|| {
            ToolError::ExecutionFailed("Merge Request tools require Workspace identity".into())
        })?;
        if matches!(self.kind, Kind::Show) {
            let value: ShowInput = parse(input)?;
            nonempty(&value.ticket)?;
            return self.show_current_merge_request(ws, &value.ticket);
        }
        let (method, path, body) = match self.kind {
            Kind::Readiness => {
                let v: ShowInput = parse(input)?;
                nonempty(&v.ticket)?;
                (
                    WorkspaceRequestMethod::Get,
                    format!("/api/w/{ws}/tickets/{}/merge-request/readiness", v.ticket),
                    None,
                )
            }
            Kind::Show => unreachable!("MergeRequestShow is handled above"),
            Kind::Open => {
                let v: OpenInput = parse(input)?;
                nonempty(&v.ticket)?;
                (
                    WorkspaceRequestMethod::Post,
                    format!("/api/w/{ws}/tickets/{}/merge-request", v.ticket),
                    Some(
                        json!({"repository_id":v.repository_id,"selector_from":v.selector_from,"selector_to":v.selector_to,"summary":v.summary}),
                    ),
                )
            }
            Kind::Complete => {
                let v: CompleteInput = parse(input)?;
                nonempty(&v.ticket)?;
                (
                    WorkspaceRequestMethod::Post,
                    format!("/api/w/{ws}/tickets/{}/merge-request/complete", v.ticket),
                    Some(
                        json!({"operation_id":v.operation_id,"approval_event_id":v.approval_event_id,"target_ref_before":v.target_ref_before,"target_ref_after":v.target_ref_after,"strategy":match v.strategy{MergeStrategyInput::FastForward=>"fast_forward",MergeStrategyInput::Merge=>"merge"},"resolution":match v.resolution{MergeResolutionInput::None=>"none",MergeResolutionInput::Clean=>"clean",MergeResolutionInput::ConflictsResolved=>"conflicts_resolved"}}),
                    ),
                )
            }
            Kind::Review => {
                let v: ReviewInput = parse(input)?;
                let ctx = self.client.reviewer_context().ok_or_else(|| {
                    ToolError::ExecutionFailed(
                        "Review submit requires injected Reviewer capability".into(),
                    )
                })?;
                (
                    WorkspaceRequestMethod::Post,
                    format!(
                        "/api/w/{ws}/tickets/{}/merge-request/reviews",
                        ctx.ticket_id
                    ),
                    Some(
                        json!({"decision":match v.decision{ReviewDecisionInput::Approve=>"approve",ReviewDecisionInput::RequestChanges=>"request_changes"},"body":v.body,"findings":v.findings.into_iter().map(|f|json!({"severity":f.severity,"code":f.code,"path":f.path,"line":f.line,"body":f.body})).collect::<Vec<_>>() }),
                    ),
                )
            }
        };
        let req = match body {
            Some(v) => WorkspaceRequest::json(method, path, v.to_string()),
            None => WorkspaceRequest::get(path),
        };
        let res = self
            .client
            .execute(req)
            .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
        if !res.is_success() {
            return Err(ToolError::ExecutionFailed(format!(
                "Merge Request API returned HTTP {}: {}",
                res.status, res.body
            )));
        }
        Ok(ToolOutput {
            summary: self.kind.name().into(),
            content: Some(res.body),
            attachments: vec![],
        })
    }
}

impl MergeRequestTool {
    fn show_current_merge_request(
        &self,
        workspace_id: &str,
        ticket: &str,
    ) -> Result<ToolOutput, ToolError> {
        let ticket_path = encode_path_segment(ticket);
        let show_response = self
            .client
            .execute(WorkspaceRequest::json(
                WorkspaceRequestMethod::Post,
                format!("/api/w/{workspace_id}/tickets/{ticket_path}/show"),
                json!({"event_limit": 1}).to_string(),
            ))
            .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?;
        if !show_response.is_success() {
            return Err(api_error("Ticket Show API", &show_response));
        }
        let projection: TicketMergeRequestProjection = serde_json::from_str(&show_response.body)
            .map_err(|error| {
                ToolError::ExecutionFailed(format!(
                    "Ticket Show API returned a malformed Merge Request projection: {error}"
                ))
            })?;
        let merge_request = projection.merge_request.ok_or_else(|| {
            ToolError::ExecutionFailed(format!("Ticket `{ticket}` has no current Merge Request"))
        })?;
        nonempty_id("merge_request_id", &merge_request.merge_request_id)?;

        let merge_request_id = encode_path_segment(&merge_request.merge_request_id);
        let response = self
            .client
            .execute(WorkspaceRequest::get(format!(
                "/api/w/{workspace_id}/merge-requests/{merge_request_id}"
            )))
            .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?;
        if !response.is_success() {
            return Err(api_error("Merge Request API", &response));
        }
        Ok(ToolOutput {
            summary: self.kind.name().into(),
            content: Some(response.body),
            attachments: vec![],
        })
    }
}

fn api_error(operation: &str, response: &crate::worker::WorkspaceResponse) -> ToolError {
    ToolError::ExecutionFailed(format!(
        "{operation} returned HTTP {}: {}",
        response.status,
        bounded_body(&response.body)
    ))
}

fn bounded_body(body: &str) -> String {
    const MAX_CHARS: usize = 4096;
    let mut chars = body.chars();
    let bounded: String = chars.by_ref().take(MAX_CHARS).collect();
    if chars.next().is_some() {
        format!("{bounded}…")
    } else {
        bounded
    }
}

fn encode_path_segment(value: &str) -> String {
    let mut encoded = String::with_capacity(value.len());
    for byte in value.bytes() {
        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
            encoded.push(byte as char);
        } else {
            use std::fmt::Write as _;
            let _ = write!(encoded, "%{byte:02X}");
        }
    }
    encoded
}

fn nonempty_id(name: &str, value: &str) -> Result<(), ToolError> {
    if value.trim().is_empty() || value.chars().any(char::is_control) {
        Err(ToolError::ExecutionFailed(format!(
            "Ticket Show API returned an invalid {name}"
        )))
    } else {
        Ok(())
    }
}

fn parse<T: serde::de::DeserializeOwned>(v: &str) -> Result<T, ToolError> {
    serde_json::from_str(v).map_err(|e| ToolError::InvalidArgument(e.to_string()))
}
fn nonempty(v: &str) -> Result<(), ToolError> {
    if v.trim().is_empty() {
        Err(ToolError::InvalidArgument(
            "ticket must not be empty".into(),
        ))
    } else {
        Ok(())
    }
}
fn definition(client: Arc<dyn WorkspaceClient>, kind: Kind) -> ToolDefinition {
    Arc::new(move || {
        (
            ToolMeta::new(kind.name())
                .description(description(kind.name()).unwrap_or("Merge Request operation."))
                .input_schema(kind.schema()),
            Arc::new(MergeRequestTool {
                client: client.clone(),
                kind,
            }) as Arc<dyn Tool>,
        )
    })
}
pub struct MergeRequestFeature {
    client: Arc<dyn WorkspaceClient>,
    config: MergeRequestFeatureConfig,
}

impl MergeRequestFeature {
    pub fn new(client: Arc<dyn WorkspaceClient>, config: MergeRequestFeatureConfig) -> Self {
        Self { client, config }
    }

    fn kinds(&self) -> impl Iterator<Item = Kind> + '_ {
        ALL_KINDS
            .into_iter()
            .filter(|kind| kind.enabled(self.config))
    }
}

impl FeatureModule for MergeRequestFeature {
    fn descriptor(&self) -> FeatureDescriptor {
        let mut descriptor = FeatureDescriptor::builtin(FEATURE_ID, FEATURE_NAME)
            .with_description(FEATURE_DESCRIPTION);
        if self.config.any() {
            descriptor = descriptor.with_instruction(workflow_instruction());
        }
        for kind in self.kinds() {
            descriptor = descriptor.with_tool(ToolDeclaration::new(
                kind.name(),
                description(kind.name()).unwrap_or("Merge Request operation."),
            ));
        }
        descriptor
    }

    fn install(&self, ctx: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> {
        if self.config.any() {
            ctx.instructions()
                .register(FeatureInstructionContribution::new(workflow_instruction()))?;
        }
        let mut tools = ctx.tools();
        for kind in self.kinds() {
            let definition = definition(self.client.clone(), kind);
            tools.register(ToolContribution::new(kind.name(), definition))?;
        }
        Ok(())
    }
}

pub fn description(n: &str) -> Option<&'static str> {
    match n {
        "MergeRequestShow" => Some("Read the selector-based Merge Request and append-only thread."),
        "MergeRequestReadinessCheck" => {
            Some("Resolve current provider refs and derive readiness from valid review events.")
        }
        "MergeRequestOpen" => {
            Some("Open a Merge Request with immutable source and target selectors.")
        }
        "MergeRequestComplete" => {
            Some("Complete using an approved review event and final target-ref evidence.")
        }
        "MergeRequestReview" => {
            Some("Submit the injected Reviewer capability result for its captured subject ref.")
        }
        _ => None,
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::feature::FeatureRegistryBuilder;
    use crate::hook::HookRegistryBuilder;
    use crate::worker::{TestWorkspaceHttpClient, WorkspaceClientError, WorkspaceResponse};
    use std::{collections::VecDeque, sync::Mutex};

    #[derive(Debug)]
    struct RecordingWorkspaceClient {
        responses: Mutex<VecDeque<WorkspaceResponse>>,
        requests: Mutex<Vec<WorkspaceRequest>>,
    }

    impl RecordingWorkspaceClient {
        fn new(responses: Vec<WorkspaceResponse>) -> Self {
            Self {
                responses: Mutex::new(responses.into()),
                requests: Mutex::new(Vec::new()),
            }
        }
    }

    impl WorkspaceClient for RecordingWorkspaceClient {
        fn workspace_id(&self) -> Option<&str> {
            Some("ws")
        }

        fn kind(&self) -> &str {
            "recording"
        }

        fn is_available(&self) -> bool {
            true
        }

        fn execute(
            &self,
            request: WorkspaceRequest,
        ) -> Result<WorkspaceResponse, WorkspaceClientError> {
            self.requests.lock().expect("request lock").push(request);
            self.responses
                .lock()
                .expect("response lock")
                .pop_front()
                .ok_or_else(|| WorkspaceClientError::Request("missing test response".into()))
        }
    }

    fn response(body: serde_json::Value) -> WorkspaceResponse {
        WorkspaceResponse {
            status: 200,
            body: body.to_string(),
        }
    }

    #[tokio::test]
    async fn show_resolves_ticket_projection_then_reads_canonical_resource() {
        let client = Arc::new(RecordingWorkspaceClient::new(vec![
            response(json!({"merge_request":{"merge_request_id":"MR/1"}})),
            response(json!({"merge_request_id":"MR/1","state":"open"})),
        ]));
        let tool = MergeRequestTool {
            kind: Kind::Show,
            client: client.clone(),
        };

        let output = tool
            .execute(r#"{"ticket":"T/1"}"#, ToolExecutionContext::default())
            .await
            .expect("show should succeed");

        assert_eq!(
            output.content.as_deref(),
            Some(r#"{"merge_request_id":"MR/1","state":"open"}"#)
        );
        let requests = client.requests.lock().expect("request lock");
        assert_eq!(requests.len(), 2);
        assert_eq!(requests[0].method, WorkspaceRequestMethod::Post);
        assert_eq!(requests[0].path, "/api/w/ws/tickets/T%2F1/show");
        assert_eq!(requests[1].method, WorkspaceRequestMethod::Get);
        assert_eq!(requests[1].path, "/api/w/ws/merge-requests/MR%2F1");
    }

    #[tokio::test]
    async fn show_fails_closed_when_ticket_has_no_current_merge_request() {
        let client = Arc::new(RecordingWorkspaceClient::new(vec![response(
            json!({"merge_request":null}),
        )]));
        let tool = MergeRequestTool {
            kind: Kind::Show,
            client: client.clone(),
        };

        let error = tool
            .execute(r#"{"ticket":"T1"}"#, ToolExecutionContext::default())
            .await
            .expect_err("missing Merge Request must fail");

        assert!(error.to_string().contains("no current Merge Request"));
        assert_eq!(client.requests.lock().expect("request lock").len(), 1);
    }

    fn install(config: MergeRequestFeatureConfig) -> (Vec<String>, Vec<String>) {
        let client: Arc<dyn WorkspaceClient> =
            Arc::new(TestWorkspaceHttpClient::new("workspace", "http://unused"));
        let mut pending_tools = Vec::new();
        let mut hook_builder = HookRegistryBuilder::default();
        let report = FeatureRegistryBuilder::new()
            .with_module(MergeRequestFeature::new(client, config))
            .install_into_pending(&mut pending_tools, &mut hook_builder);
        assert!(!report.has_errors(), "{}", report.error_message());
        (
            report.installed_tool_names(),
            report
                .installed_instruction_contributions()
                .into_iter()
                .map(|instruction| instruction.prompt_ref)
                .collect(),
        )
    }

    fn tool_names(config: MergeRequestFeatureConfig) -> Vec<String> {
        install(config).0
    }

    #[test]
    fn flags_define_the_exact_registered_tool_surface() {
        let coder = MergeRequestFeatureConfig {
            show: true,
            open: true,
            ..Default::default()
        };
        assert_eq!(tool_names(coder), ["MergeRequestShow", "MergeRequestOpen"]);

        let reviewer = MergeRequestFeatureConfig {
            show: true,
            review: true,
            ..Default::default()
        };
        assert_eq!(
            tool_names(reviewer),
            ["MergeRequestShow", "MergeRequestReview"]
        );

        let orchestrator = MergeRequestFeatureConfig {
            show: true,
            readiness_check: true,
            complete: true,
            ..Default::default()
        };
        assert_eq!(
            tool_names(orchestrator),
            [
                "MergeRequestShow",
                "MergeRequestReadinessCheck",
                "MergeRequestComplete"
            ]
        );
        assert_eq!(install(coder).1, [FEATURE_PROMPT_REF]);
        let unspecified = install(MergeRequestFeatureConfig::default());
        assert!(unspecified.0.is_empty());
        assert!(unspecified.1.is_empty());
    }

    #[test]
    fn schemas_hide_revision_and_commit_authority() {
        let schemas = [
            schemars::schema_for!(OpenInput),
            schemars::schema_for!(CompleteInput),
        ];
        for s in schemas {
            let j = serde_json::to_string(&s).unwrap();
            for banned in [
                "revision_id",
                "attempt_id",
                "base_commit",
                "head_commit",
                "source_commit",
                "result_commit",
            ] {
                assert!(!j.contains(banned), "{banned} in {j}")
            }
        }
    }
}
