using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Architecture;
using Speckle.Converters.Common;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Converters.RevitShared.Settings;

namespace Speckle.Connectors.Revit.HostApp;

/// <summary>
/// Class that unpacks a given set of selection elements into atomic objects.
/// </summary>
public class ElementUnpacker
{
  private readonly RevitContext _revitContext;
  private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;

  public ElementUnpacker(RevitContext revitContext, IConverterSettingsStore<RevitConversionSettings> converterSettings)
  {
    _revitContext = revitContext;
    _converterSettings = converterSettings;
  }

  /// <summary>
  /// Unpacks a random set of revit objects into atomic objects. It currently unpacks groups recurisvely, nested families into atomic family instances.
  /// This method will also "pack" curtain walls if necessary (ie, if mullions or panels are selected without their parent curtain wall, they are sent independently; if the parent curtain wall is selected, they will be removed out as the curtain wall will include all its children).
  /// </summary>
  /// <param name="selectionElements"></param>
  /// <param name="doc"> We use the nullable document (happiness level 5/10) for the sake of linked models - bc we use this function in 2 different places <br/>
  /// 1- RootObjectBuilder with linked model document - otherwise we cannot unpack elements from correct document.<br/>
  /// 2- Evicting the cache while introducing the settings</param>
  /// <returns></returns>
  public IEnumerable<Element> UnpackSelectionForConversion(IEnumerable<Element> selectionElements, Document? doc = null)
  {
    // Note: steps kept separate on purpose.
    // Step 1: unpack groups
    var atomicObjects = UnpackElements(selectionElements, doc);

    // Step 2: pack curtain wall elements, once we know the full extent of our flattened item list.
    // The behaviour we're looking for:
    // If parent wall is part of selection, does not select individual elements out. Otherwise, selects individual elements (Panels, Mullions) as atomic objects.
    // NOTE: this also conditionally "packs" stacked wall elements if their parent is present. See detailed note inside the function.
    return PackCurtainWallElementsAndStackedWalls(atomicObjects, doc);
  }

  /// <summary>
  /// Unpacks input element ids into their subelements, eg groups and nested family instances
  /// </summary>
  /// <param name="objectIds"></param>
  /// <returns></returns>
  /// <remarks>
  /// This is used to invalidate object ids in the send conversion cache when the selected object id is only the parent element id
  /// </remarks>
  public IEnumerable<string> GetUnpackedElementIds(IEnumerable<string> objectIds)
  {
    var doc = _revitContext.UIApplication?.ActiveUIDocument.Document!;
    var docElements = doc.GetElements(objectIds);
    return UnpackSelectionForConversion(docElements).Select(o => o.UniqueId).ToList();
  }

  // We use the nullable document (happiness level 5/10) for the sake of linked models - bc we use this function in 2 different places
  // 1- RootObjectBuilder with linked model document - otherwise we cannot unpack elements from correct document.
  // 2- Evicting the cache while introducing the settings
  private List<Element> UnpackElements(IEnumerable<Element> elements, Document? doc = null)
  {
    var unpackedElements = new List<Element>(); // note: could be a hashset/map so we prevent duplicates (?)
    if (doc == null)
    {
      doc = _revitContext.UIApplication?.ActiveUIDocument.Document!;
    }

    foreach (var element in elements)
    {
      // UNPACK: Groups
      if (element is Group g)
      {
        // When a group is from a linked model, GetMemberIds may behave differently
        // We add null checks to handle cases where elements can't be properly resolved
        // POC: this might screw up generating hosting rel generation here, because nested families in groups get flattened out by GetMemberIds().
        var groupElements = g.GetMemberIds().Select(doc.GetElement).Where(el => el != null);
        unpackedElements.AddRange(UnpackElements(groupElements));
      }
      else if (element is BaseArray baseArray)
      {
        // For arrays, collect both copied and original members with null checks
        // This handles cases where some elements might not resolve in linked contexts
        var arrayElements = baseArray.GetCopiedMemberIds().Select(doc.GetElement).Where(el => el != null);
        var originalElements = baseArray.GetOriginalMemberIds().Select(doc.GetElement).Where(el => el != null);
        unpackedElements.AddRange(UnpackElements(arrayElements));
        unpackedElements.AddRange(UnpackElements(originalElements));
      }
      // UNPACK: Family instances (as they potentially have nested families inside)
      else if (element is FamilyInstance familyInstance)
      {
        var familyElements = familyInstance
          .GetSubComponentIds()
          .Select(doc.GetElement)
          .Where(el => el != null)
          .ToArray();

        if (familyElements.Length != 0)
        {
          unpackedElements.AddRange(UnpackElements(familyElements));
        }

        unpackedElements.Add(familyInstance);
      }
      else if (element is MultistoryStairs multistoryStairs)
      {
        var stairs = multistoryStairs.GetAllStairsIds().Select(doc.GetElement).Where(el => el != null);
        unpackedElements.AddRange(UnpackElements(stairs));
      }
      else
      {
        unpackedElements.Add(element);
      }
    }
    // Why filtering for duplicates? Well, well, well... it's related to the comment above on groups: if a group
    // contains a nested family, GetMemberIds() will return... duplicates of the exploded family components.

    // Add null check before GroupBy to prevent NullReferenceException when processing linked models with groups
    // This ensures we don't try to access .Id on any null elements that might have been added during the unpacking process
    return unpackedElements.Where(el => el != null).GroupBy(el => el.Id).Select(g => g.First()).ToList(); // no disinctBy in here sadly.
  }

  // We use the nullable document (happiness level 5/10) for the sake of linked models - bc we use this function in 2 different places
  // 1- RootObjectBuilder with linked model document - otherwise we cannot unpack elements from correct document.
  // 2- Evicting the cache while introducing the settings
  private List<Element> PackCurtainWallElementsAndStackedWalls(List<Element> elements, Document? doc = null)
  {
    var ids = elements.Select(el => el.Id).ToArray();
    if (doc == null)
    {
      doc = _revitContext.UIApplication?.ActiveUIDocument.Document!;
    }
    elements.RemoveAll(element =>
      (element is Mullion { Host: not null } m && ids.Contains(m.Host.Id))
      || (
        element is Panel { Host: not null } p
        && ids.Contains(p.Host.Id)
        && doc.GetElement(p.Host.Id) is not CurtainSystem // don't remove panels when host is CurtainSystem [CNX-1884](https://linear.app/speckle/issue/CNX-1884/revit-curtain-system-not-sending-properly)
      )
      || (
        element is FamilyInstance { Host: not null } f
        && doc.GetElement(f.Host.Id) is Wall { CurtainGrid: not null }
        && ids.Contains(f.Host.Id)
      )
      // NOTE: It is required to explicitly skip stacked wall members because, when getting objects from a view,
      // the api will return the wall parent and its stacked children walls separately. This does not happen
      // via selection. Via category ("Walls") we do not get any parent wall, but just the components of the stacked wall separately.
      // If you wonder why revit is driving people to insanity, this is one of those moments.
      // See [CNX-851: Stacked Wall Duplicate Geometry or Materials not applied](https://linear.app/speckle/issue/CNX-851/stacked-wall-duplicate-geometry-or-materials-not-applied)
      || (element is Wall { IsStackedWallMember: true } wall && ids.Contains(wall.StackedWallOwnerId))
    );
    return elements;
  }

  /// <summary>
  /// Given a set of atomic elements, it will return a list of all their ids as well as their subelements. This currently handles <b>curtain walls</b> and <b>stacked walls</b>.
  /// This might not be an exhaustive list of valid objects with "subelements" in revit, and will need revisiting.
  /// </summary>
  /// <param name="elements"></param>
  /// <returns></returns>
  public List<string> GetElementsAndSubelementIdsFromAtomicObjects(List<Element> elements)
  {
    var ids = new HashSet<string>();
    foreach (var element in elements)
    {
      switch (element)
      {
        case Wall wall:
          if (wall.CurtainGrid is { } grid)
          {
            foreach (var mullionId in grid.GetMullionIds())
            {
              ids.Add(mullionId.ToString());
            }
            foreach (var panelId in grid.GetPanelIds())
            {
              ids.Add(panelId.ToString());
            }
          }
          else if (wall.IsStackedWall)
          {
            foreach (var stackedWallId in wall.GetStackedWallMemberIds())
            {
              ids.Add(stackedWallId.ToString());
            }
          }
          break;
        case FootPrintRoof footPrintRoof:
          if (footPrintRoof.CurtainGrids is { } gs)
          {
            foreach (CurtainGrid roofGrid in gs)
            {
              foreach (var mullionId in roofGrid.GetMullionIds())
              {
                ids.Add(mullionId.ToString());
              }
              foreach (var panelId in roofGrid.GetPanelIds())
              {
                ids.Add(panelId.ToString());
              }
            }
          }
          break;
        default:
          break;
      }

      ids.Add(element.Id.ToString());
    }

    return ids.ToList();
  }
}
