// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

//! Resolution writer extension trait.
//!
//! This module provides [`ResolutionWriteExt`], for writing resolution mappings
//! to the `SymbolResolution` partition. Do not call `writer.index_entry()`
//! directly for symbolique partitions.
//!
//! # Architecture (ADR0003)
//!
//! Unlike other stages, resolution does **not** create new symbol shapes.
//! It links existing references (in FileSymbols/LinkedSymbols/MergedSymbols)
//! to their resolved target definitions (also in those partitions).
//!
//! ```text
//! writer.write_resolution()
//!     │
//!     └─► writer.index_entry::<SymbolResolution>(
//!             reference_path,
//!             ResolutionEntry { target_path, target: existing_handle }
//!         )
//! ```
//!
//! # Usage
//!
//! Import the trait and call methods on `PartitionWriteContextRef`:
//!
//! ```ignore
//! use symbolique::ResolutionWriteExt;
//!
//! fn resolve_symbols<P>(
//!     writer: &mut PartitionWriteContextRef<'_, P>,
//!     target_handle: RecordHandle<Symbols<MyValue, MyIdent, String>>,
//! )
//! where
//!     P: Partitions,
//!     P::Stores: HasPartition<SymbolResolution<MyValue, MyIdent, String>>,
//! {
//!     writer.write_resolution::<MyValue, MyIdent, String>(
//!         "file|ref|my_func".to_string(),  // reference path
//!         "file|fn|my_func".to_string(),   // target definition path
//!         target_handle,                    // handle to target in Symbols
//!     );
//! }
//! ```
//!
//! # Key Difference from Other Stages
//!
//! - Does **not** call `writer.store::<Symbols>()`
//! - Takes an existing `RecordHandle<Symbols>` as input
//! - Only creates `ResolutionEntry` index entries

use {
  crate::{
    core::{Ident, SymbolPath, Value, Visibility},
    partitions::{
      records::ResolutionEntry, resolution::SymbolResolution, symbols::Symbols,
    },
  },
  laburnum::database::{
    HasPartition, PartitionWriteContextRef, RecordHandle,
    storage::Partitions,
  },
};

/// Extension trait for writing resolution mappings to partitions.
///
/// Provides a method for recording that a reference resolves to a specific
/// target definition. Unlike other stages, resolution does not create new
/// symbol shapes - it links existing shapes.
///
/// # Type Parameters (on methods)
///
/// - `V`: Value type for literals
/// - `I`: Identifier type for names
/// - `Path`: Symbol path type (used for index keys and symbol paths)
///
/// # Example
///
/// ```ignore
/// use symbolique::ResolutionWriteExt;
///
/// fn resolve_reference(
///     writer: &mut PartitionWriteContextRef<'_, MyPartitions>,
///     reference_path: String,
///     target_path: String,
///     target_handle: RecordHandle<Symbols<MyValue, MyIdent, String>>,
/// ) {
///     writer.write_resolution::<MyValue, MyIdent, String>(
///         reference_path,
///         target_path,
///         target_handle,
///     );
/// }
/// ```
pub trait ResolutionWriteExt<P: Partitions> {
  /// Clear all resolutions for a prefix before re-resolving.
  ///
  /// Call this with the file/workspace prefix before writing new resolutions
  /// to ensure old data is removed.
  fn clear_resolutions<V, I, Path, S>(
    &mut self,
    prefix: &Path,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    S: Visibility,
    P::Stores: HasPartition<SymbolResolution<V, I, Path, S>>,
    SymbolResolution<V, I, Path, S>: laburnum::database::partitions::SortKeyOf<P>
      + laburnum::database::Partition<
        SortKey = Path,
        IndexEntry = ResolutionEntry<V, I, Path, S>,
      >
      + 'static;

  /// Write a resolution mapping.
  ///
  /// Records that a reference at `reference_path` resolves to the definition
  /// at `target_path`, with `target_handle` pointing to the target's shape
  /// in the `Symbols` partition.
  ///
  /// # Arguments
  ///
  /// - `reference_path`: Path of the reference being resolved (used as index key)
  /// - `target_path`: Path of the target definition
  /// - `target_handle`: Handle to the target's shape in the `Symbols` partition
  fn write_resolution<V, I, Path, S>(
    &mut self,
    reference_path: Path,
    target_path: Path,
    target_handle: RecordHandle<Symbols<V, I, Path, S>>,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    S: Visibility,
    P::Stores: HasPartition<SymbolResolution<V, I, Path, S>>,
    SymbolResolution<V, I, Path, S>: laburnum::database::partitions::SortKeyOf<P>
      + laburnum::database::Partition<
        SortKey = Path,
        IndexEntry = ResolutionEntry<V, I, Path, S>,
      >;

  /// Write a found-but-inaccessible resolution: the reference reached a
  /// definition that exists but whose visibility denies the reference site.
  /// The target handle is retained so go-to-definition still works.
  fn write_inaccessible<V, I, Path, S>(
    &mut self,
    reference_path: Path,
    target_path: Path,
    target_handle: RecordHandle<Symbols<V, I, Path, S>>,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    S: Visibility,
    P::Stores: HasPartition<SymbolResolution<V, I, Path, S>>,
    SymbolResolution<V, I, Path, S>: laburnum::database::partitions::SortKeyOf<P>
      + laburnum::database::Partition<
        SortKey = Path,
        IndexEntry = ResolutionEntry<V, I, Path, S>,
      >;

  /// Write an unresolved resolution: no definition exists for `target_path`.
  fn write_unresolved<V, I, Path, S>(
    &mut self,
    reference_path: Path,
    target_path: Path,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    S: Visibility,
    P::Stores: HasPartition<SymbolResolution<V, I, Path, S>>,
    SymbolResolution<V, I, Path, S>: laburnum::database::partitions::SortKeyOf<P>
      + laburnum::database::Partition<
        SortKey = Path,
        IndexEntry = ResolutionEntry<V, I, Path, S>,
      >;
}

impl<P: Partitions> ResolutionWriteExt<P> for PartitionWriteContextRef<'_, P> {
  fn clear_resolutions<V, I, Path, S>(
    &mut self,
    prefix: &Path,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    S: Visibility,
    P::Stores: HasPartition<SymbolResolution<V, I, Path, S>>,
    SymbolResolution<V, I, Path, S>: laburnum::database::partitions::SortKeyOf<P>
      + laburnum::database::Partition<
        SortKey = Path,
        IndexEntry = ResolutionEntry<V, I, Path, S>,
      >
      + 'static,
  {
    self.clear_prefix::<SymbolResolution<V, I, Path, S>>(
      prefix.clone(),
    );
  }

  fn write_resolution<V, I, Path, S>(
    &mut self,
    reference_path: Path,
    target_path: Path,
    target_handle: RecordHandle<Symbols<V, I, Path, S>>,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    S: Visibility,
    P::Stores: HasPartition<SymbolResolution<V, I, Path, S>>,
    SymbolResolution<V, I, Path, S>: laburnum::database::partitions::SortKeyOf<P>
      + laburnum::database::Partition<
        SortKey = Path,
        IndexEntry = ResolutionEntry<V, I, Path, S>,
      >,
  {
    let entry = ResolutionEntry::resolved(target_path, target_handle);
    self.index_entry::<SymbolResolution<V, I, Path, S>>(
      reference_path.clone(),
      entry,
    );
  }

  fn write_inaccessible<V, I, Path, S>(
    &mut self,
    reference_path: Path,
    target_path: Path,
    target_handle: RecordHandle<Symbols<V, I, Path, S>>,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    S: Visibility,
    P::Stores: HasPartition<SymbolResolution<V, I, Path, S>>,
    SymbolResolution<V, I, Path, S>: laburnum::database::partitions::SortKeyOf<P>
      + laburnum::database::Partition<
        SortKey = Path,
        IndexEntry = ResolutionEntry<V, I, Path, S>,
      >,
  {
    let entry = ResolutionEntry::inaccessible(target_path, target_handle);
    self.index_entry::<SymbolResolution<V, I, Path, S>>(
      reference_path.clone(),
      entry,
    );
  }

  fn write_unresolved<V, I, Path, S>(
    &mut self,
    reference_path: Path,
    target_path: Path,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    S: Visibility,
    P::Stores: HasPartition<SymbolResolution<V, I, Path, S>>,
    SymbolResolution<V, I, Path, S>: laburnum::database::partitions::SortKeyOf<P>
      + laburnum::database::Partition<
        SortKey = Path,
        IndexEntry = ResolutionEntry<V, I, Path, S>,
      >,
  {
    let entry = ResolutionEntry::<V, I, Path, S>::not_found(target_path);
    self.index_entry::<SymbolResolution<V, I, Path, S>>(
      reference_path.clone(),
      entry,
    );
  }
}

#[cfg(test)]
mod tests {
  use crate::{
    ResolutionTarget, Symbol, SymbolVisibility,
    partitions::{
      SymbolResolution, Symbols,
      test_support::{TestPartitions, TestStores},
    },
    test_helpers::{DV, SI, TP, test_span, test_span_cache},
  };
  use laburnum::database::{
    HasPartition, PartitionWriteContextRef,
    chunk::RecordWriter,
  };
  use super::ResolutionWriteExt;
  use crate::SymboliqueWriteExt;

  fn make_writer() -> RecordWriter<TestPartitions> {
    RecordWriter::<TestPartitions>::new(laburnum::Ident::new("test"))
  }

  #[test]
  fn write_resolution_round_trip() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 0);

    {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);

      // First, write a definition to get a target handle
      let target_handle = ctx.write_symbol_definition::<DV, SI, TP, SymbolVisibility>(
        "file|fn|target".to_string(),
        span,
        SI::new("target"),
        None,
        SymbolVisibility::Public,
      );

      // Write a resolution mapping
      ctx.write_resolution::<DV, SI, TP, SymbolVisibility>(
        "file|ref|call_target".to_string(),
        "file|fn|target".to_string(),
        target_handle,
      );
    }

    let chunk = writer.build();
    let stores = chunk.storage();

    let res_store =
      <TestStores as HasPartition<SymbolResolution<DV, SI, TP>>>::store(
        stores,
      );
    let entry = res_store.index_get(&"file|ref|call_target".to_string());
    assert!(entry.is_some());

    let entry = entry.as_ref();
    assert_eq!(
      entry.map(|e| e.target_path.as_str()),
      Some("file|fn|target"),
    );

    // Verify the target handle points to the correct symbol
    let sym_store =
      <TestStores as HasPartition<Symbols<DV, SI, TP>>>::store(stores);
    let record = entry
      .and_then(|e| e.target.handle())
      .and_then(|h| sym_store.get_by_handle(&h))
      .as_ref()
      .and_then(|r| r.record())
      .cloned();
    match record {
      Some(Symbol::Definition { name, .. }) => {
        assert_eq!(name.as_str(), "target");
      }
      _ => panic!("expected Definition"),
    }
  }

  #[test]
  fn write_inaccessible_and_unresolved_round_trip() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 0);

    {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);

      let target_handle = ctx
        .write_symbol_definition::<DV, SI, TP, SymbolVisibility>(
          "file|fn|hidden".to_string(),
          span,
          SI::new("hidden"),
          None,
          SymbolVisibility::Private,
        );

      // Found, but visibility-hidden: retains the target handle.
      ctx.write_inaccessible::<DV, SI, TP, SymbolVisibility>(
        "file|ref|to_hidden".to_string(),
        "file|fn|hidden".to_string(),
        target_handle,
      );

      // No definition at all: no handle.
      ctx.write_unresolved::<DV, SI, TP, SymbolVisibility>(
        "file|ref|to_missing".to_string(),
        "file|fn|missing".to_string(),
      );
    }

    let chunk = writer.build();
    let stores = chunk.storage();
    let res_store =
      <TestStores as HasPartition<SymbolResolution<DV, SI, TP>>>::store(
        stores,
      );

    let inaccessible =
      res_store.index_get(&"file|ref|to_hidden".to_string()).expect("inaccessible entry");
    assert!(matches!(
      inaccessible.target,
      ResolutionTarget::Inaccessible(_)
    ));
    assert_eq!(inaccessible.target_path.as_str(), "file|fn|hidden");
    assert!(
      inaccessible.target.handle().is_some(),
      "inaccessible retains the target handle for go-to-definition",
    );

    let missing =
      res_store.index_get(&"file|ref|to_missing".to_string()).expect("not-found entry");
    assert!(matches!(missing.target, ResolutionTarget::NotFound));
    assert_eq!(missing.target_path.as_str(), "file|fn|missing");
    assert!(missing.target.handle().is_none());
  }

  #[test]
  fn clear_resolutions_records_prefix() {
    let mut writer = make_writer();

    {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      ctx.clear_resolutions::<DV, SI, TP, SymbolVisibility>(
        &"file|".to_string(),
      );
    }

    let prefixes = writer.clear_prefixes();
    assert_eq!(prefixes.len(), 1);
    assert_eq!(
      prefixes[0].1,
      <SymbolResolution<DV, SI, TP, SymbolVisibility> as laburnum::database::partitions::SortKeyOf<TestPartitions>>::wrap_sort_key(
        "file|".to_string(),
      )
    );
  }
}
