//! Deterministic Architecture Product direct database access smoke workflow.

use crate::error::{RuntimeError, RuntimeResult};
use crate::reports::{
    AiProjectionRecord, AiProjectionRecordType, AiProjectionView,
    ArchitectureDirectDbAccessSmokeProjection, ArchitectureDirectDbAccessSmokeReport,
    ArchitectureDirectDbAccessSmokeResult, ArchitectureDirectDbAccessSmokeScenario,
    ArchitectureSmokeStatus, AuditProjectionView, HumanReviewProjectionView, ProjectionAudience,
    ProjectionPurpose, ProjectionTrace, ProjectionViewSet, ReportEnvelope, ReportMetadata,
};
use higher_graphen_core::{
    Confidence, Id, Provenance, ReviewStatus, Severity, SourceKind, SourceRef,
};
use higher_graphen_projection::InformationLoss;
use higher_graphen_reasoning::completion::{
    detect_completion_candidates, CompletionCandidate, CompletionDetectionInput, CompletionRule,
    MissingType, SuggestedStructure,
};
use higher_graphen_reasoning::invariant::{
    CheckResult, CheckTargetKind, Invariant, InvariantScope, Violation,
};
use higher_graphen_reasoning::obstruction::{
    Counterexample, Obstruction, ObstructionExplanation, ObstructionType, RequiredResolution,
};
use higher_graphen_structure::space::{
    Cell, ComplexType, InMemorySpaceStore, Incidence, IncidenceOrientation, Space,
};

const WORKFLOW_NAME: &str = "architecture_direct_db_access_smoke";
const REPORT_SCHEMA: &str = "highergraphen.architecture.direct_db_access_smoke.report.v1";
const REPORT_TYPE: &str = "architecture_direct_db_access_smoke";
const REPORT_VERSION: u32 = 1;

const ARCHITECTURE_SPACE: &str = "space:architecture-product-smoke";
const ARCHITECTURE_CONTEXT: &str = "context:architecture-review";
const ORDER_CONTEXT: &str = "context:orders";
const BILLING_CONTEXT: &str = "context:billing";
const ORDER_SERVICE: &str = "cell:order-service";
const BILLING_SERVICE: &str = "cell:billing-service";
const BILLING_DB: &str = "cell:billing-db";
const ORDER_READS_BILLING_DB: &str = "incidence:order-service-reads-billing-db";
const BILLING_OWNS_BILLING_DB: &str = "incidence:billing-service-owns-billing-db";
const NO_CROSS_CONTEXT_DB_ACCESS: &str = "invariant:no-cross-context-direct-database-access";
const DIRECT_DB_ACCESS_OBSTRUCTION: &str = "obstruction:order-service-direct-billing-db-access";
const BILLING_STATUS_API_CANDIDATE: &str = "candidate:billing-status-api";
const BILLING_STATUS_API_CELL: &str = "cell:billing-status-api";

/// Runs the deterministic Architecture Product direct database access smoke workflow.
pub fn run_architecture_direct_db_access_smoke(
) -> RuntimeResult<ArchitectureDirectDbAccessSmokeReport> {
    let scenario = build_architecture_smoke_scenario()?;
    let check_result = direct_database_access_violation(&scenario.invariant)?;
    let obstruction = obstruction_for_violation(&check_result)?;
    let completion_candidates = propose_billing_status_api(&obstruction)?;
    ensure_unreviewed_candidate(&completion_candidates)?;

    let report_scenario = report_scenario(scenario);
    let result = report_result(check_result, obstruction, completion_candidates)?;
    let projection = report_projection(&result)?;

    Ok(ReportEnvelope {
        schema: REPORT_SCHEMA.to_owned(),
        report_type: REPORT_TYPE.to_owned(),
        report_version: REPORT_VERSION,
        metadata: ReportMetadata::architecture_direct_db_access_smoke(),
        scenario: report_scenario,
        result,
        projection,
    })
}

struct ArchitectureSmokeScenario {
    space: Space,
    cells: Vec<Cell>,
    incidences: Vec<Incidence>,
    invariant: Invariant,
}

fn build_architecture_smoke_scenario() -> RuntimeResult<ArchitectureSmokeScenario> {
    let mut store = InMemorySpaceStore::new();
    store.insert_space(Space::new(
        id(ARCHITECTURE_SPACE)?,
        "Architecture Product Smoke",
    ))?;

    let cells = insert_architecture_cells(&mut store)?;
    let incidences = insert_architecture_incidences(&mut store)?;
    let architecture_graph = store.construct_complex(
        id("complex:architecture-product-smoke")?,
        id(ARCHITECTURE_SPACE)?,
        "Architecture dependency and ownership graph",
        ComplexType::TypedGraph,
        cells.iter().map(|cell| cell.id.clone()),
        incidences.iter().map(|incidence| incidence.id.clone()),
    )?;
    debug_assert_eq!(architecture_graph.max_dimension, 0);

    let space = store
        .space(&id(ARCHITECTURE_SPACE)?)
        .ok_or_else(|| missing_inserted("space"))?
        .clone();

    Ok(ArchitectureSmokeScenario {
        space,
        cells,
        incidences,
        invariant: no_cross_context_database_access_invariant()?,
    })
}

fn insert_architecture_cells(store: &mut InMemorySpaceStore) -> RuntimeResult<Vec<Cell>> {
    let cells = vec![
        Cell::new(id(ORDER_SERVICE)?, id(ARCHITECTURE_SPACE)?, 0, "service")
            .with_label("Order Service")
            .with_context(id(ORDER_CONTEXT)?),
        Cell::new(id(BILLING_SERVICE)?, id(ARCHITECTURE_SPACE)?, 0, "service")
            .with_label("Billing Service")
            .with_context(id(BILLING_CONTEXT)?),
        Cell::new(id(BILLING_DB)?, id(ARCHITECTURE_SPACE)?, 0, "database")
            .with_label("Billing DB")
            .with_context(id(BILLING_CONTEXT)?),
    ];

    for cell in &cells {
        store.insert_cell(cell.clone())?;
    }

    Ok(cells)
}

fn insert_architecture_incidences(store: &mut InMemorySpaceStore) -> RuntimeResult<Vec<Incidence>> {
    let incidences = vec![
        Incidence::new(
            id(ORDER_READS_BILLING_DB)?,
            id(ARCHITECTURE_SPACE)?,
            id(ORDER_SERVICE)?,
            id(BILLING_DB)?,
            "reads_database",
            IncidenceOrientation::Directed,
        ),
        Incidence::new(
            id(BILLING_OWNS_BILLING_DB)?,
            id(ARCHITECTURE_SPACE)?,
            id(BILLING_SERVICE)?,
            id(BILLING_DB)?,
            "owns_database",
            IncidenceOrientation::Directed,
        ),
    ];

    for incidence in &incidences {
        store.insert_incidence(incidence.clone())?;
    }

    Ok(incidences)
}

fn no_cross_context_database_access_invariant() -> RuntimeResult<Invariant> {
    let mut invariant = Invariant::new(
        id(NO_CROSS_CONTEXT_DB_ACCESS)?,
        "No cross-context direct database access",
        InvariantScope::Contexts {
            space_id: id(ARCHITECTURE_SPACE)?,
            context_ids: vec![id(ORDER_CONTEXT)?, id(BILLING_CONTEXT)?],
        },
        Severity::Critical,
        provenance()?,
    );
    invariant.description = Some(
        "A component must not directly access a database owned by another context.".to_owned(),
    );
    Ok(invariant)
}

fn direct_database_access_violation(invariant: &Invariant) -> RuntimeResult<CheckResult> {
    let violation = Violation::new(
        "Order Service directly accesses Billing DB, which is owned by Billing Service.",
        Severity::Critical,
    )
    .with_location_cells(vec![id(ORDER_SERVICE)?, id(BILLING_DB)?])
    .with_location_contexts(vec![id(ORDER_CONTEXT)?, id(BILLING_CONTEXT)?]);

    Ok(CheckResult::violated(
        CheckTargetKind::Invariant,
        invariant.id.clone(),
        violation,
    ))
}

fn obstruction_for_violation(check_result: &CheckResult) -> RuntimeResult<Obstruction> {
    let violation = check_result
        .violation()
        .ok_or_else(|| missing_violation("obstruction"))?;
    let explanation = ObstructionExplanation::new("Order Service directly accesses Billing DB")?
        .with_details(&violation.message)?;
    let counterexample = Counterexample::new("Direct cross-context database access is present")?
        .with_assignment("accessor", "Order Service")?
        .with_assignment("database", "Billing DB")?
        .with_assignment("owner", "Billing Service")?
        .with_path_cell(id(ORDER_SERVICE)?)
        .with_path_cell(id(BILLING_DB)?)
        .with_context(id(ORDER_CONTEXT)?)
        .with_context(id(BILLING_CONTEXT)?);
    let resolution = RequiredResolution::new(
        "Expose billing status through Billing Service and remove the direct Billing DB read.",
    )?
    .with_target_cell(id(ORDER_SERVICE)?)
    .with_target_cell(id(BILLING_SERVICE)?)
    .with_target_cell(id(BILLING_DB)?);

    Ok(Obstruction::new(
        id(DIRECT_DB_ACCESS_OBSTRUCTION)?,
        id(ARCHITECTURE_SPACE)?,
        ObstructionType::InvariantViolation,
        explanation,
        violation.severity,
        provenance()?,
    )
    .with_location_cell(id(ORDER_SERVICE)?)
    .with_location_cell(id(BILLING_DB)?)
    .with_location_context(id(ORDER_CONTEXT)?)
    .with_location_context(id(BILLING_CONTEXT)?)
    .with_counterexample(counterexample)
    .with_required_resolution(resolution))
}

fn propose_billing_status_api(
    obstruction: &Obstruction,
) -> RuntimeResult<Vec<CompletionCandidate>> {
    let suggested_api = SuggestedStructure::new(
        "api",
        "Billing Service should expose a billing status query API.",
    )?
    .with_structure_id(id(BILLING_STATUS_API_CELL)?)
    .with_related_ids(vec![
        id(BILLING_SERVICE)?,
        id(ORDER_SERVICE)?,
        id(BILLING_DB)?,
    ]);
    let rule = CompletionRule::new(
        id("rule:billing-status-api")?,
        id(BILLING_STATUS_API_CANDIDATE)?,
        MissingType::Cell,
        suggested_api,
        "The obstruction shows Order Service needs billing status without direct database access.",
        Confidence::new(0.9)?,
    )?
    .with_context_ids(vec![id(ARCHITECTURE_CONTEXT)?])
    .with_inferred_from(vec![obstruction.id.clone(), id(ORDER_READS_BILLING_DB)?]);
    let input = CompletionDetectionInput::new(id(ARCHITECTURE_SPACE)?, vec![rule])
        .with_context_ids(vec![id(ARCHITECTURE_CONTEXT)?]);

    Ok(detect_completion_candidates(input)?.into_candidates())
}

fn ensure_unreviewed_candidate(candidates: &[CompletionCandidate]) -> RuntimeResult<()> {
    match candidates {
        [candidate] if candidate.review_status == ReviewStatus::Unreviewed => Ok(()),
        [candidate] => Err(RuntimeError::workflow_construction(
            WORKFLOW_NAME,
            format!(
                "completion candidate {} has review status {:?}",
                candidate.id, candidate.review_status
            ),
        )),
        _ => Err(RuntimeError::workflow_construction(
            WORKFLOW_NAME,
            format!(
                "expected exactly one completion candidate, got {}",
                candidates.len()
            ),
        )),
    }
}

fn report_scenario(scenario: ArchitectureSmokeScenario) -> ArchitectureDirectDbAccessSmokeScenario {
    ArchitectureDirectDbAccessSmokeScenario {
        space_id: scenario.space.id,
        workflow_context_id: id_static(ARCHITECTURE_CONTEXT),
        context_ids: vec![id_static(ORDER_CONTEXT), id_static(BILLING_CONTEXT)],
        cells: scenario.cells,
        incidences: scenario.incidences,
        invariant_id: scenario.invariant.id,
        invariant_name: scenario.invariant.name,
    }
}

fn report_result(
    check_result: CheckResult,
    obstruction: Obstruction,
    completion_candidates: Vec<CompletionCandidate>,
) -> RuntimeResult<ArchitectureDirectDbAccessSmokeResult> {
    if !check_result.is_violated() {
        return Err(RuntimeError::workflow_construction(
            WORKFLOW_NAME,
            "deterministic smoke scenario must produce a violated check result",
        ));
    }

    Ok(ArchitectureDirectDbAccessSmokeResult {
        status: ArchitectureSmokeStatus::ViolationDetected,
        violated_invariant_id: id(NO_CROSS_CONTEXT_DB_ACCESS)?,
        check_result,
        obstructions: vec![obstruction],
        completion_candidates,
    })
}

fn report_projection(
    result: &ArchitectureDirectDbAccessSmokeResult,
) -> RuntimeResult<ArchitectureDirectDbAccessSmokeProjection> {
    let source_ids = projection_source_ids(result)?;
    let human_loss = InformationLoss::declared(
        "Projection summarizes the full space, invariant check, obstruction, and completion candidate.",
        source_ids.clone(),
    )?;
    let ai_loss = InformationLoss::declared(
        "AI view keeps source identifiers, provenance, confidence, and review status but omits full nested scenario payloads.",
        source_ids.clone(),
    )?;
    let audit_loss = InformationLoss::declared(
        "Audit trace records represented source identifiers and view coverage but omits full object payloads.",
        source_ids.clone(),
    )?;
    let summary =
        "Order Service directly reads Billing DB across Orders and Billing context boundaries."
            .to_owned();
    let recommended_actions = vec![
        "Route billing status access through Billing Service or a Billing Service API.".to_owned(),
        "Remove the direct Order Service read from Billing DB.".to_owned(),
    ];
    let human_review = HumanReviewProjectionView {
        audience: ProjectionAudience::Human,
        purpose: ProjectionPurpose::ArchitectureReview,
        summary,
        recommended_actions,
        source_ids: source_ids.clone(),
        information_loss: vec![human_loss],
    };
    let ai_view = AiProjectionView {
        audience: ProjectionAudience::AiAgent,
        purpose: ProjectionPurpose::ArchitectureReview,
        records: ai_projection_records(result),
        source_ids: source_ids.clone(),
        information_loss: vec![ai_loss],
    };
    let audit_trace = AuditProjectionView {
        audience: ProjectionAudience::Audit,
        purpose: ProjectionPurpose::AuditTrace,
        source_ids,
        information_loss: vec![audit_loss],
        traces: audit_traces(ai_view.source_ids.clone()),
    };

    Ok(ProjectionViewSet {
        audience: human_review.audience,
        purpose: human_review.purpose,
        summary: human_review.summary.clone(),
        recommended_actions: human_review.recommended_actions.clone(),
        information_loss: human_review.information_loss.clone(),
        source_ids: human_review.source_ids.clone(),
        human_review,
        ai_view,
        audit_trace,
    })
}

fn projection_source_ids(result: &ArchitectureDirectDbAccessSmokeResult) -> RuntimeResult<Vec<Id>> {
    let mut ids = vec![
        id(ARCHITECTURE_SPACE)?,
        id(ORDER_SERVICE)?,
        id(BILLING_SERVICE)?,
        id(BILLING_DB)?,
        id(ORDER_READS_BILLING_DB)?,
        id(BILLING_OWNS_BILLING_DB)?,
        id(NO_CROSS_CONTEXT_DB_ACCESS)?,
    ];
    push_unique(&mut ids, result.violated_invariant_id.clone());
    for obstruction in &result.obstructions {
        push_unique(&mut ids, obstruction.id.clone());
        push_unique(&mut ids, obstruction.space_id.clone());
        for cell_id in &obstruction.location_cell_ids {
            push_unique(&mut ids, cell_id.clone());
        }
        for context_id in &obstruction.location_context_ids {
            push_unique(&mut ids, context_id.clone());
        }
    }
    for candidate in &result.completion_candidates {
        for source_id in completion_candidate_source_ids(candidate) {
            push_unique(&mut ids, source_id);
        }
    }
    Ok(ids)
}

fn ai_projection_records(
    result: &ArchitectureDirectDbAccessSmokeResult,
) -> Vec<AiProjectionRecord> {
    let mut records = Vec::new();
    let check_source_ids = result
        .check_result
        .violation()
        .map(|violation| {
            let mut ids = vec![result.check_result.target_id().clone()];
            for cell_id in &violation.location_cell_ids {
                push_unique(&mut ids, cell_id.clone());
            }
            for context_id in &violation.location_context_ids {
                push_unique(&mut ids, context_id.clone());
            }
            ids
        })
        .unwrap_or_else(|| vec![result.check_result.target_id().clone()]);
    records.push(AiProjectionRecord {
        id: result.check_result.target_id().clone(),
        record_type: AiProjectionRecordType::CheckResult,
        summary: result
            .check_result
            .violation()
            .map(|violation| violation.message.clone())
            .unwrap_or_else(|| "Invariant check completed without violation.".to_owned()),
        source_ids: check_source_ids,
        confidence: None,
        review_status: None,
        severity: result
            .check_result
            .violation()
            .map(|violation| violation.severity),
        provenance: None,
    });

    records.extend(result.obstructions.iter().map(|obstruction| {
        let mut source_ids = vec![obstruction.id.clone(), obstruction.space_id.clone()];
        for cell_id in &obstruction.location_cell_ids {
            push_unique(&mut source_ids, cell_id.clone());
        }
        for context_id in &obstruction.location_context_ids {
            push_unique(&mut source_ids, context_id.clone());
        }
        AiProjectionRecord {
            id: obstruction.id.clone(),
            record_type: AiProjectionRecordType::Obstruction,
            summary: obstruction.explanation.summary.clone(),
            source_ids,
            confidence: Some(obstruction.provenance.confidence),
            review_status: Some(obstruction.provenance.review_status),
            severity: Some(obstruction.severity),
            provenance: Some(obstruction.provenance.clone()),
        }
    }));

    records.extend(
        result
            .completion_candidates
            .iter()
            .map(|candidate| AiProjectionRecord {
                id: candidate.id.clone(),
                record_type: AiProjectionRecordType::CompletionCandidate,
                summary: candidate.suggested_structure.summary.clone(),
                source_ids: completion_candidate_source_ids(candidate),
                confidence: Some(candidate.confidence),
                review_status: Some(candidate.review_status),
                severity: None,
                provenance: None,
            }),
    );

    records
}

fn completion_candidate_source_ids(candidate: &CompletionCandidate) -> Vec<Id> {
    let mut ids = vec![candidate.id.clone(), candidate.space_id.clone()];
    if let Some(structure_id) = &candidate.suggested_structure.structure_id {
        push_unique(&mut ids, structure_id.clone());
    }
    for related_id in &candidate.suggested_structure.related_ids {
        push_unique(&mut ids, related_id.clone());
    }
    for inferred_from in &candidate.inferred_from {
        push_unique(&mut ids, inferred_from.clone());
    }
    ids
}

fn audit_traces(source_ids: Vec<Id>) -> Vec<ProjectionTrace> {
    source_ids
        .into_iter()
        .map(|source_id| ProjectionTrace {
            role: source_role(&source_id).to_owned(),
            source_id,
            represented_in: vec![
                "human_review".to_owned(),
                "ai_view".to_owned(),
                "audit_trace".to_owned(),
            ],
        })
        .collect()
}

fn source_role(source_id: &Id) -> &'static str {
    match source_id.as_str() {
        ARCHITECTURE_SPACE => "space",
        ORDER_SERVICE | BILLING_SERVICE | BILLING_DB | BILLING_STATUS_API_CELL => "cell",
        ORDER_READS_BILLING_DB | BILLING_OWNS_BILLING_DB => "incidence",
        ORDER_CONTEXT | BILLING_CONTEXT | ARCHITECTURE_CONTEXT => "context",
        NO_CROSS_CONTEXT_DB_ACCESS => "invariant",
        DIRECT_DB_ACCESS_OBSTRUCTION => "obstruction",
        BILLING_STATUS_API_CANDIDATE => "completion_candidate",
        _ => "source",
    }
}

fn push_unique(ids: &mut Vec<Id>, id: Id) {
    if !ids.contains(&id) {
        ids.push(id);
    }
}

fn provenance() -> RuntimeResult<Provenance> {
    Ok(Provenance::new(
        SourceRef::new(SourceKind::Document),
        Confidence::new(1.0)?,
    ))
}

fn id(value: &str) -> RuntimeResult<Id> {
    Ok(Id::new(value)?)
}

fn id_static(value: &str) -> Id {
    Id::new(value).expect("static runtime workflow id should be valid")
}

fn missing_inserted(target: &str) -> RuntimeError {
    RuntimeError::workflow_construction(
        WORKFLOW_NAME,
        format!("{target} was not available after insertion"),
    )
}

fn missing_violation(target: &str) -> RuntimeError {
    RuntimeError::workflow_construction(
        WORKFLOW_NAME,
        format!("{target} requires a violated check result"),
    )
}
