use std::{
    collections::HashMap,
    path::PathBuf,
};

use http::Method;

use crate::{
    request::{RequestProtocol, VariableStore},
    schema::SchemaRegistry,
};

use super::{
    Rule, context,
    declarations::{
        DeclarationSpanIndex, parse_request_collection, register_declaration,
        remember_declaration_span, validate_schema_registry,
    },
    legacy_header::normalize_legacy_collection_header,
    preprocessor,
    protocol::{
        SessionRequestTarget, ensure_session_protocol_compatible,
        infer_request_protocol, infer_syntax_ws_send_kind,
        is_graphql_document_content_type, remember_session_target,
        resolve_request_target, syntax_protocol_context_json,
        validate_graphql_variables_json, validate_mcp_object_json,
    },
    request::{
        ScannedRequest, parse_assertions, parse_response_captures,
        scan_request_block, validate_fragment_guards,
        validate_graphql_request, validate_mcp_call,
        validate_protocol_directives, validate_sse_action,
        validate_ws_action,
    },
    spans::template_error_to_span,
    variables::{
        assign_global_variable_for_validation,
        assign_request_variable_for_validation,
    },
};

#[derive(Debug, Clone)]
pub struct SyntaxRequestSummary {
    pub index: usize,
    pub description: String,
    pub method: String,
    pub url: String,
    pub protocol: String,
    pub protocol_context: Option<serde_json::Value>,
}

#[derive(Debug, Clone)]
pub struct SyntaxSummary {
    pub name: String,
    pub description: String,
    pub requests: Vec<SyntaxRequestSummary>,
}

pub fn inspect_collection_syntax(
    input: &str,
    working_dir: PathBuf,
) -> Result<SyntaxSummary, pest::error::Error<Rule>> {
    let preprocessed = preprocessor::preprocess(input, &working_dir).map_err(|err| {
        pest::error::Error::new_from_span(
            pest::error::ErrorVariant::CustomError {
                message: err.to_string(),
            },
            pest::Span::new(input, 0, input.len()).unwrap(),
        )
    })?;
    let preprocessed = normalize_legacy_collection_header(preprocessed.as_str());

    let collection = parse_request_collection(preprocessed.as_str())?;

    let mut name = String::new();
    let mut description = String::new();
    let mut requests = Vec::new();
    let mut global_store = VariableStore::new();
    let mut schema_registry = SchemaRegistry::default();
    let mut declaration_spans = DeclarationSpanIndex::new();
    let mut session_targets: HashMap<String, SessionRequestTarget> = HashMap::new();

    for pair in collection.into_inner() {
        match pair.as_rule() {
            Rule::collection_name => {
                name = pair.as_str().trim().to_string();
            }
            Rule::collection_description => {
                description.push_str(pair.as_str().trim());
            }
            Rule::variable => {
                let span = pair.as_span();
                let mut inner_pairs = pair.into_inner();
                let key = inner_pairs.next().unwrap().as_str().trim().to_string();
                let value = inner_pairs.next().unwrap().as_str().to_string();

                assign_global_variable_for_validation(&mut global_store, key, value.as_str())
                    .map_err(|err| template_error_to_span(err, span.clone()))?;
            }
            Rule::header | Rule::query | Rule::callback => {}
            Rule::declaration => {
                remember_declaration_span(&mut declaration_spans, &pair);
                register_declaration(&mut schema_registry, pair)?;
            }
            Rule::requests => {
                validate_schema_registry(
                    &schema_registry,
                    preprocessed.as_str(),
                    &declaration_spans,
                )?;
                for (index, request_pair) in pair.into_inner().enumerate() {
                    requests.push(inspect_request_syntax(
                        index,
                        request_pair,
                        &global_store,
                        &schema_registry,
                        &mut session_targets,
                    )?);
                }
            }
            _ => {}
        }
    }

    Ok(SyntaxSummary {
        name,
        description,
        requests,
    })
}

fn inspect_request_syntax(
    index: usize,
    pair: pest::iterators::Pair<Rule>,
    collection_store: &VariableStore,
    schema_registry: &SchemaRegistry,
    session_targets: &mut HashMap<String, SessionRequestTarget>,
) -> Result<SyntaxRequestSummary, pest::error::Error<Rule>> {
    let scanned_request = scan_request_block(
        pair,
        collection_store,
        Vec::new(),
        |store, key, raw_value, span| {
            assign_request_variable_for_validation(store, key, raw_value)
                .map_err(|err| template_error_to_span(err, span))
        },
    )?;

    let has_graphql_directives = scanned_request.has_graphql_directives();
    let has_mcp_directives = scanned_request.has_mcp_directives();
    let has_ws_directives = scanned_request.has_ws_directives();
    let has_sse_directives = scanned_request.has_sse_directives();
    let body_seen = scanned_request.body_seen();

    let ScannedRequest {
        request_span,
        store,
        description_raw: description,
        method,
        url_raw: url,
        header_raw: _,
        query_raw: _,
        form_text_raw,
        form_file_raw,
        protocol_raw,
        session_name_raw,
        operation_name_raw,
        graphql_variables_raw,
        mcp_call_raw,
        mcp_tool_raw,
        mcp_arguments_raw,
        ws_send_raw,
        sse_receive_seen,
        sse_within_raw,
        mcp_protocol_version_raw,
        mcp_client_name_raw,
        mcp_client_version_raw,
        mcp_capabilities_raw,
        body_raw: _,
        body_content_type_raw,
        callback_src_raw: _,
        dependencies: _,
        response_capture_raw,
        assertion_raw,
        fragment_includes_raw: _,
        fragment_guard_raw,
    } = scanned_request;

    let scalar_map = store.clone_scalars();
    let session_name = session_name_raw
        .as_ref()
        .map(|(raw, _)| context::inject_from_variable(raw.as_str(), &scalar_map));

    let protocol = infer_request_protocol(
        protocol_raw
            .as_ref()
            .map(|(protocol, span)| (protocol.as_str(), span.clone())),
        session_name.as_deref(),
        has_mcp_directives,
        has_ws_directives,
        has_sse_directives,
        session_targets,
    )?;

    ensure_session_protocol_compatible(
        session_name.as_deref(),
        protocol,
        session_targets,
        request_span.clone(),
    )?;

    validate_protocol_directives(
        protocol,
        has_graphql_directives,
        has_mcp_directives,
        has_ws_directives,
        has_sse_directives,
        request_span.clone(),
    )?;

    let ws_send_kind = if matches!(protocol, RequestProtocol::Ws) {
        infer_syntax_ws_send_kind(
            request_span.clone(),
            ws_send_raw
                .as_ref()
                .map(|(raw, span)| (raw.as_str(), span.clone())),
            body_content_type_raw
                .as_ref()
                .map(|(raw, span)| (raw.as_str(), span.clone())),
            body_seen,
            &scalar_map,
        )?
    } else {
        None
    };

    let (method, url) = resolve_request_target(
        method,
        url,
        session_name.as_deref(),
        protocol,
        session_targets,
        request_span.clone(),
    )?;

    if matches!(protocol, RequestProtocol::Graphql) {
        validate_graphql_request(
            request_span.clone(),
            method.clone(),
            !form_text_raw.is_empty() || !form_file_raw.is_empty(),
            body_seen,
            body_content_type_raw
                .as_ref()
                .map(|(value, _)| is_graphql_document_content_type(value.as_str()))
                .unwrap_or(false),
            body_content_type_raw
                .as_ref()
                .map(|(_, span)| span.clone())
                .unwrap_or_else(|| request_span.clone()),
            || {
                if let Some((raw, span)) = graphql_variables_raw.as_ref() {
                    validate_graphql_variables_json(raw.as_str(), span.clone())?;
                }
                Ok(())
            },
        )?;
    }

    if matches!(protocol, RequestProtocol::Mcp) {
        if method != Method::POST {
            return Err(pest::error::Error::<Rule>::new_from_span(
                pest::error::ErrorVariant::CustomError {
                    message: "MCP-over-HTTP requests currently require POST".to_string(),
                },
                request_span.clone(),
            ));
        }

        if body_seen || body_content_type_raw.is_some() {
            let span = body_content_type_raw
                .as_ref()
                .map(|(_, span)| span.clone())
                .unwrap_or_else(|| request_span.clone());
            return Err(pest::error::Error::<Rule>::new_from_span(
                pest::error::ErrorVariant::CustomError {
                    message: "MCP-over-HTTP requests do not support explicit body or content type blocks".to_string(),
                },
                span,
            ));
        }

        validate_mcp_call(
            request_span.clone(),
            mcp_call_raw.as_ref().map(|(value, _)| value.as_str()),
            mcp_tool_raw.is_some(),
            mcp_arguments_raw
                .as_ref()
                .map(|(value, span)| (value.as_str(), span.clone())),
            mcp_protocol_version_raw.is_some(),
            mcp_client_name_raw.is_some(),
            mcp_client_version_raw.is_some(),
            mcp_capabilities_raw
                .as_ref()
                .map(|(value, span)| (value.as_str(), span.clone())),
            mcp_call_raw
                .as_ref()
                .map(|(_, span)| span.clone())
                .unwrap_or_else(|| request_span.clone()),
            |raw, field, span| validate_mcp_object_json(raw, field, span),
        )?;
    }

    if matches!(protocol, RequestProtocol::Sse) {
        validate_sse_action(
            request_span.clone(),
            session_name.is_some(),
            method.clone(),
            (body_seen || body_content_type_raw.is_some()).then(|| {
                (
                    "SSE requests do not support explicit body or content type blocks",
                    body_content_type_raw
                        .as_ref()
                        .map(|(_, span)| span.clone())
                        .unwrap_or_else(|| request_span.clone()),
                )
            }),
            sse_receive_seen,
            sse_within_raw
                .as_ref()
                .map(|(within, span)| (within.as_str(), span.clone())),
            sse_within_raw
                .as_ref()
                .map(|(_, span)| span.clone())
                .unwrap_or_else(|| request_span.clone()),
        )?;
    }

    if matches!(protocol, RequestProtocol::Ws) {
        validate_ws_action(
            request_span.clone(),
            session_name.is_some(),
            method.clone(),
            None,
            sse_receive_seen,
            ws_send_raw.is_some(),
            ws_send_raw
                .as_ref()
                .map(|(_, span)| span.clone())
                .unwrap_or_else(|| request_span.clone()),
            ws_send_kind,
            body_seen,
            body_content_type_raw
                .as_ref()
                .map(|(_, span)| span.clone())
                .or_else(|| ws_send_raw.as_ref().map(|(_, span)| span.clone()))
                .unwrap_or_else(|| request_span.clone()),
            (body_seen || body_content_type_raw.is_some()).then(|| {
                (
                    if sse_receive_seen {
                        "WebSocket receive steps do not support explicit body or content type blocks"
                    } else {
                        "WebSocket open steps do not support explicit body or content type blocks"
                    },
                    body_content_type_raw
                        .as_ref()
                        .map(|(_, span)| span.clone())
                        .unwrap_or_else(|| request_span.clone()),
                )
            }),
            sse_within_raw
                .as_ref()
                .map(|(within, span)| (within.as_str(), span.clone())),
        )?;
    }

    let _ = parse_response_captures(response_capture_raw, &scalar_map)?;
    let _ = parse_assertions(assertion_raw, &scalar_map, schema_registry)?;
    validate_fragment_guards(fragment_guard_raw, &scalar_map, schema_registry)?;

    let description = if description.trim().is_empty() {
        "[No Description]".to_string()
    } else {
        description.trim().to_string()
    };

    let protocol_context = syntax_protocol_context_json(
        protocol,
        session_name.as_deref(),
        operation_name_raw.as_ref().map(|(value, _)| value.as_str()),
        graphql_variables_raw.as_ref().map(|(value, _)| value.as_str()),
        mcp_call_raw.as_ref().map(|(value, _)| value.as_str()),
        mcp_tool_raw.as_ref().map(|(value, _)| value.as_str()),
        mcp_arguments_raw.as_ref().map(|(value, _)| value.as_str()),
        mcp_protocol_version_raw
            .as_ref()
            .map(|(value, _)| value.as_str()),
        mcp_client_name_raw.as_ref().map(|(value, _)| value.as_str()),
        mcp_client_version_raw
            .as_ref()
            .map(|(value, _)| value.as_str()),
        mcp_capabilities_raw.as_ref().map(|(value, _)| value.as_str()),
        sse_receive_seen,
        ws_send_kind,
        sse_within_raw.as_ref().map(|(value, _)| value.as_str()),
    );

    remember_session_target(
        session_targets,
        session_name.as_deref(),
        protocol,
        &method,
        &url,
    );

    Ok(SyntaxRequestSummary {
        index,
        description,
        method: method.as_str().to_string(),
        url,
        protocol: protocol.as_str().to_string(),
        protocol_context,
    })
}