use serde::Serialize;
use thiserror::Error;

use crate::diff::Change;
use crate::manifest::{ObjectType, SchemaBindingFacet};
use crate::model::{DefaultPrivKey, GrantKey};
use crate::ownership::{
    ManagedScope, MembershipKey, OwnershipIndex, SchemaFacetKey, describe_change, grant_schema_name,
};
use crate::visual::VisualManagedScope;

pub const BUNDLE_PLAN_SCHEMA_VERSION: &str = "pgroles.bundle_plan.v1";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlanOutputMode {
    Full,
    Redacted,
}

#[derive(Debug, Clone)]
pub struct BundleReportContext<'a> {
    pub ownership: &'a OwnershipIndex,
    pub managed_scope: &'a ManagedScope,
}

#[derive(Debug, Error)]
pub enum BundlePlanError {
    #[error("missing managed owner for change: {change}")]
    MissingOwner { change: String },

    #[error("bundle change is missing required scope details: {change}")]
    InvalidChange { change: String },
}

#[derive(Debug, Error)]
pub enum BundlePlanRenderError {
    #[error(transparent)]
    Plan(#[from] BundlePlanError),

    #[error(transparent)]
    Json(#[from] serde_json::Error),
}

#[derive(Debug, Clone, Serialize)]
pub struct BundlePlanJson {
    pub schema_version: String,
    pub managed_scope: VisualManagedScope,
    pub changes: Vec<AnnotatedPlanChange>,
}

#[derive(Debug, Clone, Serialize)]
pub struct AnnotatedPlanChange {
    pub category: BundleChangeCategory,
    pub owner: BundleChangeOwner,
    pub change: Change,
}

#[derive(Debug, Clone, Serialize)]
pub struct BundleChangeOwner {
    pub document: String,
    pub managed_key: ManagedOwnershipKey,
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BundleChangeCategory {
    Role,
    Schema,
    Grant,
    DefaultPrivilege,
    Membership,
    Retirement,
}

#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ManagedOwnershipKey {
    Role {
        name: String,
    },
    SchemaFacet {
        schema: String,
        facet: SchemaBindingFacet,
    },
    Grant {
        role: String,
        object_type: ObjectType,
        schema: Option<String>,
        name: Option<String>,
    },
    DefaultPrivilege {
        owner: String,
        schema: String,
        on_type: ObjectType,
        grantee: String,
    },
    Membership {
        role: String,
        member: String,
    },
}

pub fn shape_plan_changes(changes: &[Change], mode: PlanOutputMode) -> Vec<Change> {
    match mode {
        PlanOutputMode::Full => changes.to_vec(),
        PlanOutputMode::Redacted => changes
            .iter()
            .map(|change| match change {
                Change::SetPassword { name, .. } => Change::SetPassword {
                    name: name.clone(),
                    password: "[REDACTED]".to_string(),
                },
                other => other.clone(),
            })
            .collect(),
    }
}

pub fn render_plan_json(
    changes: &[Change],
    mode: PlanOutputMode,
) -> Result<String, serde_json::Error> {
    serde_json::to_string_pretty(&shape_plan_changes(changes, mode))
}

pub fn build_bundle_plan(
    changes: &[Change],
    context: &BundleReportContext<'_>,
    mode: PlanOutputMode,
) -> Result<BundlePlanJson, BundlePlanError> {
    let shaped_changes = shape_plan_changes(changes, mode);
    let annotated_changes = shaped_changes
        .iter()
        .map(|change| {
            Ok(AnnotatedPlanChange {
                category: bundle_change_category(change),
                owner: lookup_bundle_change_owner(change, context.ownership)?,
                change: change.clone(),
            })
        })
        .collect::<Result<Vec<_>, BundlePlanError>>()?;

    Ok(BundlePlanJson {
        schema_version: BUNDLE_PLAN_SCHEMA_VERSION.to_string(),
        managed_scope: VisualManagedScope::from(context.managed_scope),
        changes: annotated_changes,
    })
}

pub fn render_bundle_plan_json(
    changes: &[Change],
    context: &BundleReportContext<'_>,
    mode: PlanOutputMode,
) -> Result<String, BundlePlanRenderError> {
    let plan = build_bundle_plan(changes, context, mode)?;
    Ok(serde_json::to_string_pretty(&plan)?)
}

fn lookup_bundle_change_owner(
    change: &Change,
    ownership: &OwnershipIndex,
) -> Result<BundleChangeOwner, BundlePlanError> {
    match change {
        Change::CreateRole { name, .. }
        | Change::AlterRole { name, .. }
        | Change::SetComment { name, .. }
        | Change::SetPassword { name, .. }
        | Change::DropRole { name } => ownership
            .roles
            .get(name)
            .cloned()
            .map(|document| BundleChangeOwner {
                document,
                managed_key: ManagedOwnershipKey::Role { name: name.clone() },
            })
            .ok_or_else(|| BundlePlanError::MissingOwner {
                change: describe_change(change),
            }),
        Change::TerminateSessions { role } | Change::DropOwned { role } => ownership
            .roles
            .get(role)
            .cloned()
            .map(|document| BundleChangeOwner {
                document,
                managed_key: ManagedOwnershipKey::Role { name: role.clone() },
            })
            .ok_or_else(|| BundlePlanError::MissingOwner {
                change: describe_change(change),
            }),
        Change::ReassignOwned { from_role, .. } => ownership
            .roles
            .get(from_role)
            .cloned()
            .map(|document| BundleChangeOwner {
                document,
                managed_key: ManagedOwnershipKey::Role {
                    name: from_role.clone(),
                },
            })
            .ok_or_else(|| BundlePlanError::MissingOwner {
                change: describe_change(change),
            }),
        Change::CreateSchema { name, .. } => {
            lookup_bundle_schema_owner_or_bindings(name, ownership, change)
        }
        Change::AlterSchemaOwner { name, .. }
        | Change::EnsureSchemaOwnerPrivileges { name, .. } => {
            lookup_bundle_schema_facet(name, SchemaBindingFacet::Owner, ownership, change)
        }
        Change::Grant {
            role,
            object_type,
            schema,
            name,
            ..
        }
        | Change::Revoke {
            role,
            object_type,
            schema,
            name,
            ..
        } => {
            let grant_key = GrantKey {
                role: role.clone(),
                object_type: *object_type,
                schema: schema.clone(),
                name: name.clone(),
            };

            if let Some(document) = ownership.grants.get(&grant_key) {
                return Ok(BundleChangeOwner {
                    document: document.clone(),
                    managed_key: ManagedOwnershipKey::Grant {
                        role: grant_key.role.clone(),
                        object_type: grant_key.object_type,
                        schema: grant_key.schema.clone(),
                        name: grant_key.name.clone(),
                    },
                });
            }

            if *object_type == ObjectType::Database {
                return ownership
                    .roles
                    .get(role)
                    .cloned()
                    .map(|document| BundleChangeOwner {
                        document,
                        managed_key: ManagedOwnershipKey::Role { name: role.clone() },
                    })
                    .ok_or_else(|| BundlePlanError::MissingOwner {
                        change: describe_change(change),
                    });
            }

            let schema_name =
                grant_schema_name(&grant_key).ok_or_else(|| BundlePlanError::InvalidChange {
                    change: describe_change(change),
                })?;
            lookup_bundle_schema_facet(
                &schema_name,
                SchemaBindingFacet::Bindings,
                ownership,
                change,
            )
        }
        Change::SetDefaultPrivilege {
            owner,
            schema,
            on_type,
            grantee,
            ..
        }
        | Change::RevokeDefaultPrivilege {
            owner,
            schema,
            on_type,
            grantee,
            ..
        } => {
            let key = DefaultPrivKey {
                owner: owner.clone(),
                schema: schema.clone(),
                on_type: *on_type,
                grantee: grantee.clone(),
            };

            if let Some(document) = ownership.default_privileges.get(&key) {
                return Ok(BundleChangeOwner {
                    document: document.clone(),
                    managed_key: ManagedOwnershipKey::DefaultPrivilege {
                        owner: key.owner.clone(),
                        schema: key.schema.clone(),
                        on_type: key.on_type,
                        grantee: key.grantee.clone(),
                    },
                });
            }

            lookup_bundle_schema_facet(schema, SchemaBindingFacet::Bindings, ownership, change)
        }
        Change::AddMember { role, member, .. } | Change::RemoveMember { role, member } => {
            let key = MembershipKey {
                role: role.clone(),
                member: member.clone(),
            };

            if let Some(document) = ownership.memberships.get(&key) {
                return Ok(BundleChangeOwner {
                    document: document.clone(),
                    managed_key: ManagedOwnershipKey::Membership {
                        role: role.clone(),
                        member: member.clone(),
                    },
                });
            }

            ownership
                .roles
                .get(role)
                .cloned()
                .map(|document| BundleChangeOwner {
                    document,
                    managed_key: ManagedOwnershipKey::Role { name: role.clone() },
                })
                .ok_or_else(|| BundlePlanError::MissingOwner {
                    change: describe_change(change),
                })
        }
    }
}

fn lookup_bundle_schema_owner_or_bindings(
    schema: &str,
    ownership: &OwnershipIndex,
    change: &Change,
) -> Result<BundleChangeOwner, BundlePlanError> {
    lookup_bundle_schema_facet(schema, SchemaBindingFacet::Owner, ownership, change).or_else(|_| {
        lookup_bundle_schema_facet(schema, SchemaBindingFacet::Bindings, ownership, change)
    })
}

fn lookup_bundle_schema_facet(
    schema: &str,
    facet: SchemaBindingFacet,
    ownership: &OwnershipIndex,
    change: &Change,
) -> Result<BundleChangeOwner, BundlePlanError> {
    let facet_key = SchemaFacetKey {
        schema: schema.to_string(),
        facet,
    };

    ownership
        .schema_facets
        .get(&facet_key)
        .cloned()
        .map(|document| BundleChangeOwner {
            document,
            managed_key: ManagedOwnershipKey::SchemaFacet {
                schema: schema.to_string(),
                facet,
            },
        })
        .ok_or_else(|| BundlePlanError::MissingOwner {
            change: describe_change(change),
        })
}

fn bundle_change_category(change: &Change) -> BundleChangeCategory {
    match change {
        Change::CreateRole { .. }
        | Change::AlterRole { .. }
        | Change::SetComment { .. }
        | Change::SetPassword { .. }
        | Change::DropRole { .. } => BundleChangeCategory::Role,
        Change::CreateSchema { .. }
        | Change::AlterSchemaOwner { .. }
        | Change::EnsureSchemaOwnerPrivileges { .. } => BundleChangeCategory::Schema,
        Change::Grant { .. } | Change::Revoke { .. } => BundleChangeCategory::Grant,
        Change::SetDefaultPrivilege { .. } | Change::RevokeDefaultPrivilege { .. } => {
            BundleChangeCategory::DefaultPrivilege
        }
        Change::AddMember { .. } | Change::RemoveMember { .. } => BundleChangeCategory::Membership,
        Change::ReassignOwned { .. }
        | Change::DropOwned { .. }
        | Change::TerminateSessions { .. } => BundleChangeCategory::Retirement,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::composition::{self, PolicyDocument};
    use crate::diff::diff;
    use crate::model::RoleGraph;

    #[test]
    fn render_plan_json_redacts_passwords_in_redacted_mode() {
        let changes = vec![Change::SetPassword {
            name: "app".to_string(),
            password: "super-secret".to_string(),
        }];

        let redacted = shape_plan_changes(&changes, PlanOutputMode::Redacted);
        assert_eq!(
            redacted[0],
            Change::SetPassword {
                name: "app".to_string(),
                password: "[REDACTED]".to_string(),
            }
        );

        let json =
            render_plan_json(&changes, PlanOutputMode::Redacted).expect("json should render");
        assert!(json.contains("[REDACTED]"));
        assert!(!json.contains("super-secret"));
    }

    #[test]
    fn bundle_plan_json_contract_is_versioned_and_typed() {
        let bundle = composition::parse_policy_bundle(
            r#"
sources:
  - file: app.yaml
"#,
        )
        .expect("bundle should parse");
        let documents = vec![PolicyDocument {
            source: "app.yaml".to_string(),
            fragment: composition::parse_policy_fragment(
                r#"
policy:
  name: app
scope:
  roles: [app]
roles:
  - name: app
    login: false
"#,
            )
            .expect("fragment should parse"),
        }];
        let composed =
            composition::compose_bundle(&bundle, &documents).expect("bundle should compose");
        let changes = diff(&RoleGraph::default(), &composed.desired);

        let plan = build_bundle_plan(
            &changes,
            &composed.report_context(),
            PlanOutputMode::Redacted,
        )
        .expect("bundle plan should annotate");
        let json = serde_json::to_value(&plan).expect("bundle plan should serialize");

        assert_eq!(json["schema_version"], BUNDLE_PLAN_SCHEMA_VERSION);
        assert_eq!(json["managed_scope"]["roles"][0], "app");
        assert_eq!(json["changes"][0]["category"], "role");
        assert_eq!(json["changes"][0]["owner"]["document"], "app");
        assert_eq!(json["changes"][0]["owner"]["managed_key"]["kind"], "role");
        assert_eq!(json["changes"][0]["owner"]["managed_key"]["name"], "app");
    }

    #[test]
    fn bundle_plan_full_mode_preserves_password_values() {
        let bundle = composition::parse_policy_bundle(
            r#"
sources:
  - file: app.yaml
"#,
        )
        .expect("bundle should parse");
        let documents = vec![PolicyDocument {
            source: "app.yaml".to_string(),
            fragment: composition::parse_policy_fragment(
                r#"
policy:
  name: app
scope:
  roles: [app]
roles:
  - name: app
    login: true
"#,
            )
            .expect("fragment should parse"),
        }];
        let composed =
            composition::compose_bundle(&bundle, &documents).expect("bundle should compose");
        let changes = vec![Change::SetPassword {
            name: "app".to_string(),
            password: "super-secret".to_string(),
        }];

        let redacted = build_bundle_plan(
            &changes,
            &composed.report_context(),
            PlanOutputMode::Redacted,
        )
        .expect("redacted plan should build");
        let full = build_bundle_plan(&changes, &composed.report_context(), PlanOutputMode::Full)
            .expect("full plan should build");

        assert_eq!(
            redacted.changes[0].change,
            Change::SetPassword {
                name: "app".to_string(),
                password: "[REDACTED]".to_string(),
            }
        );
        assert_eq!(
            full.changes[0].change,
            Change::SetPassword {
                name: "app".to_string(),
                password: "super-secret".to_string(),
            }
        );
    }
}
