use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};
use serde_json::Value;
use time::OffsetDateTime;

use crate::{
    edition::OperationSecurityLevel,
    ids::{AuthProfileId, DescriptorId, OperationId, SampleId},
    protocol::{ExportMode, GraphqlOperationType, HttpMethod, Protocol},
    soap::{
        SoapBindingStyle, SoapFaultContract, SoapHeaderConfig, SoapOperationMetadata, SoapVersion,
    },
    streaming::StreamingConfig,
};

fn default_operation_category() -> String {
    "general".to_owned()
}

fn default_operation_security_level() -> OperationSecurityLevel {
    OperationSecurityLevel::Standard
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationStatus {
    Draft,
    Testing,
    Published,
    Archived,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RestTarget {
    pub base_url: String,
    pub method: HttpMethod,
    pub path_template: String,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub static_headers: BTreeMap<String, String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphqlTarget {
    pub endpoint: String,
    pub operation_type: GraphqlOperationType,
    pub operation_name: String,
    pub query_template: String,
    pub response_path: String,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrpcTarget {
    pub server_addr: String,
    pub package: String,
    pub service: String,
    pub method: String,
    pub descriptor_ref: DescriptorId,
    pub descriptor_set_b64: String,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WebsocketTarget {
    pub url: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub subprotocols: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subscribe_message_template: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub unsubscribe_message_template: Option<Value>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub static_headers: BTreeMap<String, String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SoapTarget {
    pub wsdl_ref: SampleId,
    pub service_name: String,
    pub port_name: String,
    pub operation_name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub endpoint_override: Option<String>,
    pub soap_version: SoapVersion,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub soap_action: Option<String>,
    pub binding_style: SoapBindingStyle,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub headers: Vec<SoapHeaderConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fault_contract: Option<SoapFaultContract>,
    #[serde(default)]
    pub metadata: SoapOperationMetadata,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Target {
    Rest(RestTarget),
    Graphql(GraphqlTarget),
    Grpc(GrpcTarget),
    Websocket(WebsocketTarget),
    Soap(SoapTarget),
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RetryPolicy {
    pub max_attempts: u32,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct GrpcProtocolOptions {
    pub use_tls: bool,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct WebsocketProtocolOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub heartbeat_interval_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reconnect_max_attempts: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reconnect_backoff_ms: Option<u64>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SoapProtocolOptions {
    #[serde(default)]
    pub use_tls: bool,
    #[serde(default)]
    pub validate_certificate: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ws_security_profile: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ProtocolOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub grpc: Option<GrpcProtocolOptions>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub websocket: Option<WebsocketProtocolOptions>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub soap: Option<SoapProtocolOptions>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExecutionConfig {
    pub timeout_ms: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub retry_policy: Option<RetryPolicy>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub auth_profile_ref: Option<AuthProfileId>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub headers: BTreeMap<String, String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub protocol_options: Option<ProtocolOptions>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub streaming: Option<StreamingConfig>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolExample {
    pub input: Value,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolDescription {
    pub title: String,
    pub description: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub examples: Vec<ToolExample>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Samples {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub input_json_sample_ref: Option<SampleId>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub output_json_sample_ref: Option<SampleId>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub proto_file_ref: Option<SampleId>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub descriptor_ref: Option<DescriptorId>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GeneratedDraftStatus {
    None,
    Available,
    Stale,
    Failed,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeneratedDraft {
    pub status: GeneratedDraftStatus,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub source_types: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub generated_at: Option<String>,
    pub input_schema_generated: bool,
    pub output_schema_generated: bool,
    pub input_mapping_generated: bool,
    pub output_mapping_generated: bool,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub warnings: Vec<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConfigExport {
    pub format_version: String,
    pub export_mode: ExportMode,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Operation<TSchema, TMapping> {
    pub id: OperationId,
    pub name: String,
    pub display_name: String,
    #[serde(default = "default_operation_category")]
    pub category: String,
    pub protocol: Protocol,
    #[serde(default = "default_operation_security_level")]
    pub security_level: OperationSecurityLevel,
    pub status: OperationStatus,
    pub version: u32,
    pub target: Target,
    pub input_schema: TSchema,
    pub output_schema: TSchema,
    pub input_mapping: TMapping,
    pub output_mapping: TMapping,
    pub execution_config: ExecutionConfig,
    pub tool_description: ToolDescription,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub samples: Option<Samples>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub generated_draft: Option<GeneratedDraft>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub config_export: Option<ConfigExport>,
    #[serde(with = "time::serde::rfc3339")]
    pub created_at: OffsetDateTime,
    #[serde(with = "time::serde::rfc3339")]
    pub updated_at: OffsetDateTime,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[serde(with = "time::serde::rfc3339::option")]
    pub published_at: Option<OffsetDateTime>,
}

impl<TSchema, TMapping> Operation<TSchema, TMapping> {
    pub fn tool_name(&self) -> &str {
        &self.name
    }

    pub fn is_draft(&self) -> bool {
        self.status == OperationStatus::Draft
    }

    pub fn is_published(&self) -> bool {
        self.status == OperationStatus::Published
    }

    pub fn protocol(&self) -> Protocol {
        self.protocol
    }

    pub fn auth_profile_ref(&self) -> Option<&AuthProfileId> {
        self.execution_config.auth_profile_ref.as_ref()
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use serde_json::json;
    use time::{OffsetDateTime, format_description::well_known::Rfc3339};

    use crate::{
        auth::{AuthConfig, AuthProfile, BearerAuthConfig},
        edition::OperationSecurityLevel,
        ids::{AuthProfileId, OperationId, SampleId},
        operation::{
            ConfigExport, ExecutionConfig, GraphqlTarget, Operation, OperationStatus,
            ProtocolOptions, RestTarget, Samples, SoapProtocolOptions, SoapTarget, Target,
            ToolDescription, ToolExample, WebsocketProtocolOptions, WebsocketTarget,
        },
        protocol::{AuthKind, ExportMode, GraphqlOperationType, HttpMethod, Protocol},
        soap::{
            SoapBindingStyle, SoapFaultContract, SoapHeaderConfig, SoapOperationMetadata,
            SoapVersion,
        },
    };

    fn timestamp(value: &str) -> OffsetDateTime {
        OffsetDateTime::parse(value, &Rfc3339).unwrap()
    }

    #[test]
    fn rest_target_serializes_with_kind_tag() {
        let target = Target::Rest(RestTarget {
            base_url: "https://api.example.com".to_owned(),
            method: HttpMethod::Post,
            path_template: "/v1/leads".to_owned(),
            static_headers: BTreeMap::new(),
        });

        let value = serde_json::to_value(target).unwrap();

        assert_eq!(value["kind"], "rest");
        assert_eq!(value["method"], "POST");
    }

    #[test]
    fn graphql_target_serializes_response_path() {
        let target = Target::Graphql(GraphqlTarget {
            endpoint: "https://api.example.com/graphql".to_owned(),
            operation_type: GraphqlOperationType::Mutation,
            operation_name: "CreateLead".to_owned(),
            query_template: "mutation {}".to_owned(),
            response_path: "$.response.body.data.createLead".to_owned(),
        });

        let value = serde_json::to_value(target).unwrap();

        assert_eq!(value["kind"], "graphql");
        assert_eq!(value["operation_type"], "mutation");
    }

    #[test]
    fn websocket_target_serializes_templates() {
        let target = Target::Websocket(WebsocketTarget {
            url: "wss://events.example.com/stream".to_owned(),
            subprotocols: vec!["graphql-transport-ws".to_owned()],
            subscribe_message_template: Some(json!({ "type": "subscribe" })),
            unsubscribe_message_template: Some(json!({ "type": "unsubscribe" })),
            static_headers: BTreeMap::from([("x-env".to_owned(), "test".to_owned())]),
        });

        let value = serde_json::to_value(target).unwrap();

        assert_eq!(value["kind"], "websocket");
        assert_eq!(value["subprotocols"][0], "graphql-transport-ws");
        assert_eq!(value["subscribe_message_template"]["type"], "subscribe");
    }

    #[test]
    fn soap_target_serializes_binding_metadata() {
        let target = Target::Soap(SoapTarget {
            wsdl_ref: SampleId::new("sample_wsdl_01"),
            service_name: "LeadService".to_owned(),
            port_name: "LeadPort".to_owned(),
            operation_name: "CreateLead".to_owned(),
            endpoint_override: Some("https://soap.example.com/lead".to_owned()),
            soap_version: SoapVersion::Soap12,
            soap_action: Some("urn:createLead".to_owned()),
            binding_style: SoapBindingStyle::DocumentLiteral,
            headers: vec![SoapHeaderConfig {
                name: "CorrelationId".to_owned(),
                namespace_uri: Some("urn:crm".to_owned()),
                required: true,
                value_path: Some("$.mcp.correlation_id".to_owned()),
            }],
            fault_contract: Some(SoapFaultContract {
                code_path: Some("$.Envelope.Body.Fault.Code.Value".to_owned()),
                message_path: Some("$.Envelope.Body.Fault.Reason.Text".to_owned()),
                detail_path: Some("$.Envelope.Body.Fault.Detail".to_owned()),
            }),
            metadata: SoapOperationMetadata {
                input_part_names: vec!["LeadRequest".to_owned()],
                output_part_names: vec!["LeadResponse".to_owned()],
                namespaces: vec!["urn:crm".to_owned()],
            },
        });

        let value = serde_json::to_value(target).unwrap();

        assert_eq!(value["kind"], "soap");
        assert_eq!(value["soap_version"], "soap_12");
        assert_eq!(value["binding_style"], "document_literal");
        assert_eq!(value["metadata"]["input_part_names"][0], "LeadRequest");
    }

    #[test]
    fn operation_exposes_local_domain_helpers() {
        let operation = Operation {
            id: OperationId::new("op_01"),
            name: "crm_create_lead".to_owned(),
            display_name: "Create Lead".to_owned(),
            category: "sales".to_owned(),
            protocol: Protocol::Rest,
            security_level: OperationSecurityLevel::Standard,
            status: OperationStatus::Draft,
            version: 1,
            target: Target::Rest(RestTarget {
                base_url: "https://api.example.com".to_owned(),
                method: HttpMethod::Post,
                path_template: "/v1/leads".to_owned(),
                static_headers: BTreeMap::new(),
            }),
            input_schema: json!({"type":"object"}),
            output_schema: json!({"type":"object"}),
            input_mapping: json!({"rules":[]}),
            output_mapping: json!({"rules":[]}),
            execution_config: ExecutionConfig {
                timeout_ms: 10_000,
                retry_policy: None,
                auth_profile_ref: Some(AuthProfileId::new("auth_01")),
                headers: BTreeMap::new(),
                protocol_options: Some(ProtocolOptions {
                    grpc: None,
                    websocket: Some(WebsocketProtocolOptions {
                        heartbeat_interval_ms: Some(5_000),
                        reconnect_max_attempts: Some(3),
                        reconnect_backoff_ms: Some(250),
                    }),
                    soap: Some(SoapProtocolOptions {
                        use_tls: true,
                        validate_certificate: true,
                        ws_security_profile: Some("username_token".to_owned()),
                    }),
                }),
                streaming: None,
            },
            tool_description: ToolDescription {
                title: "Create CRM lead".to_owned(),
                description: "Creates a new lead.".to_owned(),
                tags: Vec::new(),
                examples: Vec::new(),
            },
            samples: Some(Samples::default()),
            generated_draft: None,
            config_export: None,
            created_at: timestamp("2026-03-25T08:00:00Z"),
            updated_at: timestamp("2026-03-25T08:00:00Z"),
            published_at: None,
        };

        assert_eq!(operation.tool_name(), "crm_create_lead");
        assert!(operation.is_draft());
        assert!(!operation.is_published());
        assert_eq!(operation.protocol(), Protocol::Rest);
        assert_eq!(
            operation.auth_profile_ref().map(|value| value.as_str()),
            Some("auth_01")
        );
    }

    #[test]
    fn auth_profile_serializes_secret_ids_without_secret_values() {
        let profile = AuthProfile {
            id: AuthProfileId::new("auth_01"),
            workspace_id: crate::ids::WorkspaceId::new("ws_01"),
            name: "crm-prod-bearer".to_owned(),
            kind: AuthKind::Bearer,
            config: AuthConfig::Bearer(BearerAuthConfig {
                header_name: "Authorization".to_owned(),
                secret_id: crate::ids::SecretId::new("secret_crm_prod_token"),
            }),
            created_at: timestamp("2026-03-25T08:00:00Z"),
            updated_at: timestamp("2026-03-25T08:00:00Z"),
        };

        let value = serde_json::to_value(profile).unwrap();

        assert_eq!(value["kind"], "bearer");
        assert_eq!(
            value["config"]["bearer"]["secret_id"],
            "secret_crm_prod_token"
        );
    }

    #[test]
    fn operation_roundtrips_through_yaml() {
        let operation = Operation {
            id: OperationId::new("op_01"),
            name: "crm_create_lead".to_owned(),
            display_name: "Create Lead".to_owned(),
            category: "sales".to_owned(),
            protocol: Protocol::Rest,
            security_level: OperationSecurityLevel::Standard,
            status: OperationStatus::Published,
            version: 3,
            target: Target::Rest(RestTarget {
                base_url: "https://api.example.com".to_owned(),
                method: HttpMethod::Post,
                path_template: "/v1/leads".to_owned(),
                static_headers: BTreeMap::from([("X-App-Source".to_owned(), "crank".to_owned())]),
            }),
            input_schema: json!({"type":"object","fields":{"email":{"type":"string","required":true}}}),
            output_schema: json!({"type":"object","fields":{"id":{"type":"string","required":true}}}),
            input_mapping: json!({"rules":[{"source":"$.mcp.email","target":"$.request.body.email"}]}),
            output_mapping: json!({"rules":[{"source":"$.response.body.id","target":"$.output.id"}]}),
            execution_config: ExecutionConfig {
                timeout_ms: 10_000,
                retry_policy: None,
                auth_profile_ref: Some(AuthProfileId::new("auth_01")),
                headers: BTreeMap::new(),
                protocol_options: Some(ProtocolOptions::default()),
                streaming: None,
            },
            tool_description: ToolDescription {
                title: "Create CRM lead".to_owned(),
                description: "Creates a new lead.".to_owned(),
                tags: vec!["crm".to_owned()],
                examples: vec![ToolExample {
                    input: json!({"email":"user@example.com"}),
                }],
            },
            samples: Some(Samples::default()),
            generated_draft: None,
            config_export: Some(ConfigExport {
                format_version: "1".to_owned(),
                export_mode: ExportMode::Portable,
            }),
            created_at: timestamp("2026-03-25T08:00:00Z"),
            updated_at: timestamp("2026-03-25T08:10:00Z"),
            published_at: Some(timestamp("2026-03-25T08:15:00Z")),
        };

        let yaml = serde_yaml::to_string(&operation).unwrap();
        let restored: Operation<serde_json::Value, serde_json::Value> =
            serde_yaml::from_str(&yaml).unwrap();

        assert!(yaml.contains("protocol: rest"));
        assert!(yaml.contains("security_level: standard"));
        assert!(yaml.contains("export_mode: portable"));
        assert_eq!(restored, operation);
    }

    #[test]
    fn operation_yaml_deserializes_without_optional_published_at() {
        let yaml = r#"
id: op_01
name: crm_create_lead
display_name: Create Lead
category: sales
protocol: rest
security_level: standard
status: draft
version: 1
target:
  kind: rest
  base_url: https://api.example.com
  method: POST
  path_template: /v1/leads
  static_headers: {}
input_schema:
  type: object
output_schema:
  type: object
input_mapping:
  rules: []
output_mapping:
  rules: []
execution_config:
  timeout_ms: 10000
  headers: {}
tool_description:
  title: Create CRM lead
  description: Creates a new lead.
  tags: []
  examples: []
created_at: 2026-03-25T08:00:00Z
updated_at: 2026-03-25T08:10:00Z
"#;

        let restored: Operation<serde_json::Value, serde_json::Value> =
            serde_yaml::from_str(yaml).unwrap();

        assert_eq!(restored.security_level, OperationSecurityLevel::Standard);
        assert_eq!(restored.published_at, None);
    }
}
