#[derive(Debug, Clone)]
pub(crate) struct UniversalOutput {
    pub continue_processing: bool,
    pub stop_reason: Option<String>,
    pub suppress_output: bool,
    pub system_message: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct SessionStartOutput {
    pub universal: UniversalOutput,
    pub additional_context: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct PreToolUseOutput {
    pub universal: UniversalOutput,
    pub block_reason: Option<String>,
    pub additional_context: Option<String>,
    pub updated_input: Option<serde_json::Value>,
    pub invalid_reason: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PermissionRequestDecision {
    Allow,
    Deny { message: String },
}

#[derive(Debug, Clone)]
pub(crate) struct PermissionRequestOutput {
    pub universal: UniversalOutput,
    pub decision: Option<PermissionRequestDecision>,
    pub invalid_reason: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct PostToolUseOutput {
    pub universal: UniversalOutput,
    pub should_block: bool,
    pub reason: Option<String>,
    pub invalid_block_reason: Option<String>,
    pub additional_context: Option<String>,
    pub invalid_reason: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct UserPromptSubmitOutput {
    pub universal: UniversalOutput,
    pub should_block: bool,
    pub reason: Option<String>,
    pub invalid_block_reason: Option<String>,
    pub additional_context: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct StopOutput {
    pub universal: UniversalOutput,
    pub should_block: bool,
    pub reason: Option<String>,
    pub invalid_block_reason: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct PreCompactOutput {
    pub universal: UniversalOutput,
    pub invalid_reason: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct StatelessHookOutput {
    pub universal: UniversalOutput,
    pub invalid_reason: Option<String>,
}

use crate::schema::BlockDecisionWire;
use crate::schema::HookUniversalOutputWire;
use crate::schema::PermissionRequestBehaviorWire;
use crate::schema::PermissionRequestCommandOutputWire;
use crate::schema::PermissionRequestDecisionWire;
use crate::schema::PostCompactCommandOutputWire;
use crate::schema::PostToolUseCommandOutputWire;
use crate::schema::PreCompactCommandOutputWire;
use crate::schema::PreToolUseCommandOutputWire;
use crate::schema::PreToolUseDecisionWire;
use crate::schema::PreToolUsePermissionDecisionWire;
use crate::schema::SessionStartCommandOutputWire;
use crate::schema::StopCommandOutputWire;
use crate::schema::SubagentStartCommandOutputWire;
use crate::schema::SubagentStopCommandOutputWire;
use crate::schema::UserPromptSubmitCommandOutputWire;

pub(crate) fn parse_session_start(stdout: &str) -> Option<SessionStartOutput> {
    let wire: SessionStartCommandOutputWire = parse_json(stdout)?;
    Some(session_start_output(
        wire.universal,
        wire.hook_specific_output
            .and_then(|output| output.additional_context),
    ))
}

pub(crate) fn parse_subagent_start(stdout: &str) -> Option<SessionStartOutput> {
    let wire: SubagentStartCommandOutputWire = parse_json(stdout)?;
    Some(session_start_output(
        wire.universal,
        wire.hook_specific_output
            .and_then(|output| output.additional_context),
    ))
}

fn session_start_output(
    universal: HookUniversalOutputWire,
    additional_context: Option<String>,
) -> SessionStartOutput {
    SessionStartOutput {
        universal: UniversalOutput::from(universal),
        additional_context,
    }
}

pub(crate) fn parse_pre_tool_use(stdout: &str) -> Option<PreToolUseOutput> {
    let PreToolUseCommandOutputWire {
        universal: universal_wire,
        decision,
        reason,
        hook_specific_output,
    } = parse_json(stdout)?;
    let universal = UniversalOutput::from(universal_wire);
    let hook_specific_output = hook_specific_output.as_ref();
    let additional_context =
        hook_specific_output.and_then(|output| output.additional_context.clone());
    let use_hook_specific_decision = hook_specific_output.is_some_and(|output| {
        output.permission_decision.is_some()
            || output.permission_decision_reason.is_some()
            || output.updated_input.is_some()
    });
    let invalid_reason = unsupported_pre_tool_use_universal(&universal).or_else(|| {
        if use_hook_specific_decision {
            hook_specific_output.and_then(unsupported_pre_tool_use_hook_specific_output)
        } else {
            unsupported_pre_tool_use_legacy_decision(decision.as_ref(), reason.as_deref())
        }
    });
    let block_reason = if invalid_reason.is_none() {
        if use_hook_specific_decision {
            hook_specific_output.and_then(|output| match output.permission_decision {
                Some(PreToolUsePermissionDecisionWire::Deny) => output
                    .permission_decision_reason
                    .as_deref()
                    .and_then(trimmed_reason),
                _ => None,
            })
        } else {
            match decision.as_ref() {
                Some(PreToolUseDecisionWire::Block) => reason.as_deref().and_then(trimmed_reason),
                Some(PreToolUseDecisionWire::Approve) | None => None,
            }
        }
    } else {
        None
    };
    let updated_input = if invalid_reason.is_none() {
        hook_specific_output.and_then(|output| {
            matches!(
                output.permission_decision,
                Some(PreToolUsePermissionDecisionWire::Allow)
            )
            .then(|| output.updated_input.clone())
            .flatten()
        })
    } else {
        None
    };

    Some(PreToolUseOutput {
        universal,
        block_reason,
        additional_context,
        updated_input,
        invalid_reason,
    })
}

pub(crate) fn parse_permission_request(stdout: &str) -> Option<PermissionRequestOutput> {
    let wire: PermissionRequestCommandOutputWire = parse_json(stdout)?;
    let universal = UniversalOutput::from(wire.universal);
    let hook_specific_output = wire.hook_specific_output.as_ref();
    let decision = hook_specific_output.and_then(|output| output.decision.as_ref());
    let invalid_reason = unsupported_permission_request_universal(&universal).or_else(|| {
        hook_specific_output.and_then(|output| {
            unsupported_permission_request_hook_specific_output(output.decision.as_ref())
        })
    });
    let decision = if invalid_reason.is_none() {
        decision.map(permission_request_decision)
    } else {
        None
    };

    Some(PermissionRequestOutput {
        universal,
        decision,
        invalid_reason,
    })
}

pub(crate) fn parse_post_tool_use(stdout: &str) -> Option<PostToolUseOutput> {
    let wire: PostToolUseCommandOutputWire = parse_json(stdout)?;
    let universal = UniversalOutput::from(wire.universal);
    let invalid_reason = unsupported_post_tool_use_universal(&universal).or_else(|| {
        wire.hook_specific_output
            .as_ref()
            .and_then(unsupported_post_tool_use_hook_specific_output)
    });
    let should_block = matches!(wire.decision, Some(BlockDecisionWire::Block));
    let invalid_block_reason = if should_block
        && match wire.reason.as_deref() {
            Some(reason) => reason.trim().is_empty(),
            None => true,
        } {
        Some(invalid_block_message("PostToolUse"))
    } else if !should_block && universal.continue_processing && wire.reason.is_some() {
        Some("PostToolUse hook returned reason without decision".to_string())
    } else {
        None
    };
    let additional_context = wire
        .hook_specific_output
        .and_then(|output| output.additional_context);

    Some(PostToolUseOutput {
        universal,
        should_block: should_block && invalid_reason.is_none() && invalid_block_reason.is_none(),
        reason: wire.reason,
        invalid_block_reason,
        additional_context,
        invalid_reason,
    })
}

pub(crate) fn parse_pre_compact(stdout: &str) -> Option<PreCompactOutput> {
    let wire: PreCompactCommandOutputWire = parse_json(stdout)?;
    let universal = UniversalOutput::from(wire.universal);
    Some(PreCompactOutput {
        universal,
        invalid_reason: None,
    })
}

pub(crate) fn parse_post_compact(stdout: &str) -> Option<StatelessHookOutput> {
    let wire: PostCompactCommandOutputWire = parse_json(stdout)?;
    let universal = UniversalOutput::from(wire.universal);
    Some(StatelessHookOutput {
        universal,
        invalid_reason: None,
    })
}

pub(crate) fn parse_user_prompt_submit(stdout: &str) -> Option<UserPromptSubmitOutput> {
    let wire: UserPromptSubmitCommandOutputWire = parse_json(stdout)?;
    let should_block = matches!(wire.decision, Some(BlockDecisionWire::Block));
    let invalid_block_reason = if should_block
        && match wire.reason.as_deref() {
            Some(reason) => reason.trim().is_empty(),
            None => true,
        } {
        Some(invalid_block_message("UserPromptSubmit"))
    } else {
        None
    };
    let additional_context = wire
        .hook_specific_output
        .and_then(|output| output.additional_context);
    Some(UserPromptSubmitOutput {
        universal: UniversalOutput::from(wire.universal),
        should_block: should_block && invalid_block_reason.is_none(),
        reason: wire.reason,
        invalid_block_reason,
        additional_context,
    })
}

pub(crate) fn parse_stop(stdout: &str) -> Option<StopOutput> {
    let wire: StopCommandOutputWire = parse_json(stdout)?;
    Some(stop_output(
        wire.universal,
        wire.decision,
        wire.reason,
        "Stop",
    ))
}

pub(crate) fn parse_subagent_stop(stdout: &str) -> Option<StopOutput> {
    let wire: SubagentStopCommandOutputWire = parse_json(stdout)?;
    Some(stop_output(
        wire.universal,
        wire.decision,
        wire.reason,
        "SubagentStop",
    ))
}

fn stop_output(
    universal: HookUniversalOutputWire,
    decision: Option<BlockDecisionWire>,
    reason: Option<String>,
    event_name: &str,
) -> StopOutput {
    let should_block = matches!(decision, Some(BlockDecisionWire::Block));
    let invalid_block_reason = if should_block
        && match reason.as_deref() {
            Some(reason) => reason.trim().is_empty(),
            None => true,
        } {
        Some(invalid_block_message(event_name))
    } else {
        None
    };
    StopOutput {
        universal: UniversalOutput::from(universal),
        should_block: should_block && invalid_block_reason.is_none(),
        reason,
        invalid_block_reason,
    }
}

impl From<HookUniversalOutputWire> for UniversalOutput {
    fn from(value: HookUniversalOutputWire) -> Self {
        Self {
            continue_processing: value.r#continue,
            stop_reason: value.stop_reason,
            suppress_output: value.suppress_output,
            system_message: value.system_message,
        }
    }
}

fn parse_json<T>(stdout: &str) -> Option<T>
where
    T: for<'de> serde::Deserialize<'de>,
{
    let trimmed = stdout.trim();
    if trimmed.is_empty() {
        return None;
    }
    let value: serde_json::Value = serde_json::from_str(trimmed).ok()?;
    if !value.is_object() {
        return None;
    }
    serde_json::from_value(value).ok()
}

pub(crate) fn looks_like_json(stdout: &str) -> bool {
    let trimmed = stdout.trim_start();
    trimmed.starts_with('{') || trimmed.starts_with('[')
}

fn invalid_block_message(event_name: &str) -> String {
    format!("{event_name} hook returned decision:block without a non-empty reason")
}

fn unsupported_pre_tool_use_universal(universal: &UniversalOutput) -> Option<String> {
    if !universal.continue_processing {
        Some("PreToolUse hook returned unsupported continue:false".to_string())
    } else if universal.stop_reason.is_some() {
        Some("PreToolUse hook returned unsupported stopReason".to_string())
    } else if universal.suppress_output {
        Some("PreToolUse hook returned unsupported suppressOutput".to_string())
    } else {
        None
    }
}

fn unsupported_permission_request_universal(universal: &UniversalOutput) -> Option<String> {
    if !universal.continue_processing {
        Some("PermissionRequest hook returned unsupported continue:false".to_string())
    } else if universal.stop_reason.is_some() {
        Some("PermissionRequest hook returned unsupported stopReason".to_string())
    } else if universal.suppress_output {
        Some("PermissionRequest hook returned unsupported suppressOutput".to_string())
    } else {
        None
    }
}

fn unsupported_post_tool_use_universal(universal: &UniversalOutput) -> Option<String> {
    if universal.suppress_output {
        Some("PostToolUse hook returned unsupported suppressOutput".to_string())
    } else {
        None
    }
}

fn unsupported_permission_request_hook_specific_output(
    decision: Option<&PermissionRequestDecisionWire>,
) -> Option<String> {
    let decision = decision?;
    if decision.updated_input.is_some() {
        Some("PermissionRequest hook returned unsupported updatedInput".to_string())
    } else if decision.updated_permissions.is_some() {
        Some("PermissionRequest hook returned unsupported updatedPermissions".to_string())
    } else if decision.interrupt {
        Some("PermissionRequest hook returned unsupported interrupt:true".to_string())
    } else {
        None
    }
}

fn permission_request_decision(
    decision: &PermissionRequestDecisionWire,
) -> PermissionRequestDecision {
    match decision.behavior {
        PermissionRequestBehaviorWire::Allow => PermissionRequestDecision::Allow,
        PermissionRequestBehaviorWire::Deny => PermissionRequestDecision::Deny {
            message: decision
                .message
                .as_deref()
                .and_then(trimmed_reason)
                .unwrap_or_else(|| "PermissionRequest hook denied approval".to_string()),
        },
    }
}

fn unsupported_post_tool_use_hook_specific_output(
    output: &crate::schema::PostToolUseHookSpecificOutputWire,
) -> Option<String> {
    if output.updated_mcp_tool_output.is_some() {
        Some("PostToolUse hook returned unsupported updatedMCPToolOutput".to_string())
    } else {
        None
    }
}

fn unsupported_pre_tool_use_hook_specific_output(
    output: &crate::schema::PreToolUseHookSpecificOutputWire,
) -> Option<String> {
    if output.updated_input.is_some()
        && !matches!(
            output.permission_decision,
            Some(PreToolUsePermissionDecisionWire::Allow)
        )
    {
        Some("PreToolUse hook returned updatedInput without permissionDecision:allow".to_string())
    } else {
        match output.permission_decision {
            Some(PreToolUsePermissionDecisionWire::Allow) => {
                output.updated_input.is_none().then(|| {
                    "PreToolUse hook returned unsupported permissionDecision:allow".to_string()
                })
            }
            Some(PreToolUsePermissionDecisionWire::Ask) => {
                Some("PreToolUse hook returned unsupported permissionDecision:ask".to_string())
            }
            Some(PreToolUsePermissionDecisionWire::Deny) => {
                if output
                    .permission_decision_reason
                    .as_deref()
                    .and_then(trimmed_reason)
                    .is_none()
                {
                    Some(invalid_pre_tool_use_reason_message())
                } else {
                    None
                }
            }
            None => {
                if output.permission_decision_reason.is_some() {
                    Some("PreToolUse hook returned permissionDecisionReason without permissionDecision".to_string())
                } else {
                    None
                }
            }
        }
    }
}

fn unsupported_pre_tool_use_legacy_decision(
    decision: Option<&PreToolUseDecisionWire>,
    reason: Option<&str>,
) -> Option<String> {
    match decision {
        Some(PreToolUseDecisionWire::Approve) => {
            Some("PreToolUse hook returned unsupported decision:approve".to_string())
        }
        Some(PreToolUseDecisionWire::Block) => {
            if reason.and_then(trimmed_reason).is_none() {
                Some(invalid_block_message("PreToolUse"))
            } else {
                None
            }
        }
        None => {
            if reason.is_some() {
                Some("PreToolUse hook returned reason without decision".to_string())
            } else {
                None
            }
        }
    }
}

fn invalid_pre_tool_use_reason_message() -> String {
    "PreToolUse hook returned permissionDecision:deny without a non-empty permissionDecisionReason"
        .to_string()
}

fn trimmed_reason(reason: &str) -> Option<String> {
    let trimmed = reason.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.to_string())
    }
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;
    use serde_json::json;

    use super::parse_permission_request;

    #[test]
    fn permission_request_rejects_reserved_updated_input_field() {
        let parsed = parse_permission_request(
            &json!({
                "continue": true,
                "hookSpecificOutput": {
                    "hookEventName": "PermissionRequest",
                    "decision": {
                        "behavior": "allow",
                        "updatedInput": {}
                    }
                }
            })
            .to_string(),
        )
        .expect("permission request hook output should parse");

        assert_eq!(
            parsed.invalid_reason,
            Some("PermissionRequest hook returned unsupported updatedInput".to_string())
        );
    }

    #[test]
    fn permission_request_rejects_reserved_updated_permissions_field() {
        let parsed = parse_permission_request(
            &json!({
                "continue": true,
                "hookSpecificOutput": {
                    "hookEventName": "PermissionRequest",
                    "decision": {
                        "behavior": "allow",
                        "updatedPermissions": {}
                    }
                }
            })
            .to_string(),
        )
        .expect("permission request hook output should parse");

        assert_eq!(
            parsed.invalid_reason,
            Some("PermissionRequest hook returned unsupported updatedPermissions".to_string())
        );
    }

    #[test]
    fn permission_request_rejects_reserved_interrupt_field() {
        let parsed = parse_permission_request(
            &json!({
                "continue": true,
                "hookSpecificOutput": {
                    "hookEventName": "PermissionRequest",
                    "decision": {
                        "behavior": "allow",
                        "interrupt": true
                    }
                }
            })
            .to_string(),
        )
        .expect("permission request hook output should parse");

        assert_eq!(
            parsed.invalid_reason,
            Some("PermissionRequest hook returned unsupported interrupt:true".to_string())
        );
    }
}
