//
//  PickerController.swift
//  LunarDatePicker
//
//  Main picker controller for displaying calendar view
//

import Foundation
import JTAppleCalendar
import UIKit

/// Generic picker controller that handles both single date and date range selection
/// - Parameter Value: The type of value being selected (Date or PickerRange)
final class PickerController<Value: PickerValue>: UIViewController {

  // MARK: - UI Components

  /// Cancel button in navigation bar
  private lazy var cancelBarButtonItem: UIBarButtonItem = {
    let size = Constants.UI.closeIconSize
    let container = UIView(frame: CGRect(x: 0, y: 0, width: size, height: size))
    container.translatesAutoresizingMaskIntoConstraints = false
    
    let button = UIButton(type: .custom)
    button.translatesAutoresizingMaskIntoConstraints = false
    let image = UIImage(named: "location_cross")?.withRenderingMode(.alwaysTemplate)
    button.setImage(image, for: .normal)
    button.imageView?.contentMode = .scaleAspectFit
    button.tintColor = self.config.controller.cancelColor.toUIColor()
    button.addTarget(self, action: #selector(self.cancel), for: .touchUpInside)
    
    container.addSubview(button)
    
    NSLayoutConstraint.activate([
      container.widthAnchor.constraint(equalToConstant: size),
      container.heightAnchor.constraint(equalToConstant: size),
      
      button.topAnchor.constraint(equalTo: container.topAnchor),
      button.bottomAnchor.constraint(equalTo: container.bottomAnchor),
      button.leadingAnchor.constraint(equalTo: container.leadingAnchor),
      button.trailingAnchor.constraint(equalTo: container.trailingAnchor)
    ])
    
    return UIBarButtonItem(customView: container)
  }()

  /// Title label displayed in navigation bar
  private lazy var titleUI: UIView = {
    let titleLabel = UILabel()
    titleLabel.text = self.config.controller.title
    titleLabel.font = UIFont.systemFont(
      ofSize: Constants.FontSize.title,
      weight: .semibold
    )
    titleLabel.textAlignment = .center
    titleLabel.contentMode = .center
    titleLabel.textColor = self.config.controller.titleColor.toUIColor()

    return titleLabel
  }()

  /// Bottom container with confirm button
  private lazy var bottomContainer: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = self.config.controller.backgroundColor.toUIColor()
    return view
  }()

  private lazy var bottomTopBorder: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = self.config.controller.borderColor.toUIColor()
    return view
  }()

  private lazy var confirmButton: UIButton = {
    let button = UIButton(type: .system)
    button.translatesAutoresizingMaskIntoConstraints = false
    var config = UIButton.Configuration.plain()
    var title = AttributedString(self.config.controller.submitText)
    title.font = UIFont.systemFont(ofSize: Constants.FontSize.confirmButtonTitle, weight: .semibold)
    title.foregroundColor = UIColor.white
    config.attributedTitle = title
    config.contentInsets = NSDirectionalEdgeInsets(
      top: Constants.Layout.confirmButtonVerticalPadding,
      leading: 0,
      bottom: Constants.Layout.confirmButtonVerticalPadding,
      trailing: 0
    )
    config.baseForegroundColor = .white
    button.configuration = config
    button.layer.cornerRadius = 12
    button.layer.masksToBounds = true
    button.isEnabled = false
    button.alpha = 0.6
    button.addTarget(
      self,
      action: #selector(self.confirmTapped),
      for: .touchUpInside
    )
    return button
  }()

  private let confirmGradientLayer: CAGradientLayer = {
    let layer = CAGradientLayer()
    layer.startPoint = CGPoint(x: 0.0, y: 0.0)
    layer.endPoint = CGPoint(x: 1.0, y: 0.0)
    return layer
  }()

  /// Main calendar view
  private lazy var calendarView: JTACMonthView = {
    let configManager = CalendarConfigurationManager(
      config: self.config,
      minimumDate: self.privateMinimumDate,
      maximumDate: self.privateMaximumDate
    )
    let monthView = configManager.createCalendarView(for: Value.self)
    monthView.ibCalendarDelegate = self
    monthView.ibCalendarDataSource = self
    return monthView
  }()

  /// Week day names header view
  private lazy var weekView: WeekView = {
    let view = WeekView(
      calendar: self.config.calendar,
      config: self.config.weekView
    )
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
  }()

  /// Top gradient header showing "Ngày đi" / "Ngày về" and dates
  private lazy var topHeaderContainer: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.layer.masksToBounds = true
    return view
  }()

  private let topHeaderGradientLayer: CAGradientLayer = {
    let layer = CAGradientLayer()
    layer.startPoint = CGPoint(x: 0.0, y: 0.0)
    layer.endPoint = CGPoint(x: 1.0, y: 0.0)
    return layer
  }()

  private lazy var departTitleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.text = self.config.controller.fromText ?? "Ngày đi"
    label.font = UIFont.systemFont(ofSize: Constants.FontSize.headerLabelSmall, weight: .regular)
    label.textColor = UIColor.white
    return label
  }()

  private lazy var departDateLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.text = self.config.controller.notSelectedText ?? "Chưa chọn"
    label.font = UIFont.systemFont(ofSize: Constants.FontSize.headerLabelMedium, weight: .semibold)
    label.textColor = self.config.controller.secondaryTextColor.toUIColor()
    return label
  }()

  private lazy var returnTitleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.text = self.config.controller.toText ?? "Ngày về"
    label.font = UIFont.systemFont(ofSize: Constants.FontSize.headerLabelSmall, weight: .regular)
    label.textColor = UIColor.white
    return label
  }()

  private lazy var returnDateLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.text = self.config.controller.notSelectedText ?? "Chưa chọn"
    label.font = UIFont.systemFont(ofSize: Constants.FontSize.headerLabelMedium, weight: .semibold)
    label.textColor = self.config.controller.secondaryTextColor.toUIColor()
    return label
  }()

  private lazy var topHeaderDivider: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = UIColor(white: 1.0, alpha: 0.4)
    return view
  }()

  private lazy var leftDecorImageView: UIImageView = {
    let iv = UIImageView()
    iv.image = UIImage(named: "calendar_from")
    iv.translatesAutoresizingMaskIntoConstraints = false
    iv.contentMode = .scaleAspectFit
    return iv
  }()

  private lazy var rightDecorImageView: UIImageView = {
    let iv = UIImageView()
    iv.image = UIImage(named: "calendar_to")
    iv.translatesAutoresizingMaskIntoConstraints = false
    iv.contentMode = .scaleAspectFit
    return iv
  }()

  // MARK: - Properties

  /// Configuration for the picker
  internal let config: PickerConfig

  /// Reuse identifiers for collection view cells
  internal let dayCellReuseIdentifier = "DayCellReuseIdentifier"
  internal let monthHeaderReuseIdentifier = "MonthHeaderReuseIdentifier"

  /// Cache for lunar date calculations only (not UI state)
  private var lunarDateCache: [String: (day: Int, month: Int)] = [:]

  /// Reusable calendar instance for better performance
  private var calendar: Calendar {
    return self.config.calendar
  }

  /// Date constraints
  internal var privateMinimumDate: Date?
  internal var privateMaximumDate: Date?

  /// Date formatter for day labels
  private var dayFormatter = DateFormatter()

  /// Auto submit flag (derived from config)
  private var isAutoSubmit: Bool { return !self.config.controller.showSubmitButton }

  /// Date formatter for month keys (reusable)
  internal lazy var monthKeyFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM"
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.timeZone = self.config.calendar.timeZone
    return formatter
  }()

  /// Cached today date to avoid creating Date() repeatedly
  private lazy var todayDate: Date = {
    return self.config.calendar.startOfDay(for: Date())
  }()

  /// Timer to update today date at midnight
  private var todayUpdateTimer: Timer?

  /// Flag to track if cleanup has been performed
  internal var hasCleanedUp = false

  /// Flag to track if initial scroll to selected date is needed
  private var needsInitialScroll = true

  /// Currently selected value
  private var value: Value?

  /// The block to execute after "Done" button will be tapped
  public var doneHandler: ((Value?) -> Void)?

  /// The block to execute when "Cancel" button will be tapped
  public var cancelHandler: (() -> Void)?

  /// The helper to load images
  public var imageLoader: ((LDP_NativeAssetSource?, @escaping (UIImage?) -> Void) -> Void)?

  /// And initial value which will be selected by default
  public var initialValue: Value?

  /// Minimal selection date. Dates less then current will be marked as unavailable
  public var minimumDate: Date? {
    get {
      self.privateMinimumDate
    }
    set {
      self.privateMinimumDate = newValue?.startOfDay()
    }
  }

  /// Maximum selection date. Dates greater then current will be marked as unavailable
  public var maximumDate: Date? {
    get {
      self.privateMaximumDate
    }
    set {
      self.privateMaximumDate = newValue?.endOfDay()
    }
  }

  // MARK: - Lifecycle

  public init(config: PickerConfig = .default) {
    self.config = config
    self.dayFormatter.locale = config.calendar.locale
    self.dayFormatter.dateFormat = "d"
    super.init(nibName: nil, bundle: nil)
  }

  @available(*, unavailable)
  public required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override public func viewDidLoad() {
    super.viewDidLoad()
    self.configureUI()
    self.configureSubviews()
    self.configureConstraints()
    self.configureInitialState()
    self.setupCalendarContentInsets()
    self.setupTodayUpdateTimer()
    self.loadCustomIcons()
    // Prewarm lunar cache for upcoming months to reduce first-render cost
    self.prewarmLunarCache(monthsBefore: 0, monthsAfter: 2)
  }

  deinit {
    cleanup()
    cleanupTodayTimer()
  }

  private func loadCustomIcons() {
    if let loader = self.imageLoader {
      loader(self.config.controller.closeImage) { [weak self] image in
        guard let self = self, let image = image else { return }
        DispatchQueue.main.async {
          let container = self.cancelBarButtonItem.customView
          let button = container?.subviews.compactMap { $0 as? UIButton }.first
          button?.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal)
        }
      }
      loader(self.config.controller.fromImage) { [weak self] image in
        guard let self = self, let image = image else { return }
        DispatchQueue.main.async {
          self.leftDecorImageView.image = image
        }
      }
      loader(self.config.controller.toImage) { [weak self] image in
        guard let self = self, let image = image else { return }
        DispatchQueue.main.async {
          self.rightDecorImageView.image = image
        }
      }
    }
  }

  // MARK: - Public Methods

  public func present(
    above viewController: UIViewController,
    animated flag: Bool = true,
    completion: (() -> Void)? = nil
  ) {
    let navVc = UINavigationController(rootViewController: self)
    navVc.modalPresentationStyle = .formSheet
    if !self.config.controller.showSubmitButton {
      let appearance = UINavigationBarAppearance()
      appearance.configureWithTransparentBackground()
      appearance.backgroundColor = .clear
      appearance.shadowColor = .clear
      navVc.navigationBar.standardAppearance = appearance
      navVc.navigationBar.scrollEdgeAppearance = appearance
    }
    if viewController.preferredContentSize != .zero {
      navVc.preferredContentSize = viewController.preferredContentSize
    } else {
      navVc.preferredContentSize = Constants.UI.defaultPickerSize
    }

    viewController.present(navVc, animated: flag, completion: completion)
  }

  /// Cleanup method to cancel all pending month visibility callbacks
  /// Should be called when the controller is being destroyed to prevent memory leaks
  internal func cleanup() {
    // Prevent multiple cleanup calls
    guard !hasCleanedUp else { return }
    hasCleanedUp = true

    // Invalidate timers & observers early
    cleanupTodayTimer()

    // IMPORTANT: Only clear delegates in deinit after view is fully dismissed
    // Clearing them earlier causes JTAppleCalendar assertion failures
    // The library expects delegates to remain valid throughout the view lifecycle
    
    // Break delegate/dataSource links to avoid retain cycles
    self.calendarView.ibCalendarDelegate = nil
    self.calendarView.ibCalendarDataSource = nil

    // Remove gradient sublayer if still attached
    self.confirmGradientLayer.removeFromSuperlayer()

    // Break closure references
    self.doneHandler = nil
    self.cancelHandler = nil

    // Clear caches to prevent memory leaks
    lunarDateCache.removeAll()
  }

  // MARK: - Today Date Management

  /// Setup timer to update today date at midnight
  private func setupTodayUpdateTimer() {
    // Calculate seconds until next midnight
    let calendar = self.config.calendar
    let now = Date()

    guard
      let tomorrow = calendar.date(
        byAdding: .day,
        value: 1,
        to: calendar.startOfDay(for: now)
      )
    else {
      return
    }

    let timeUntilMidnight = tomorrow.timeIntervalSince(now)

    // Setup timer to fire at midnight and then every 24 hours
    todayUpdateTimer = Timer.scheduledTimer(
      withTimeInterval: timeUntilMidnight,
      repeats: false
    ) { [weak self] _ in
      self?.updateTodayDate()
      self?.setupDailyTimer()
    }

    // Add observer for app becoming active (when returning from background)
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(handleAppBecameActive),
      name: UIApplication.didBecomeActiveNotification,
      object: nil
    )
  }

  /// Setup daily timer that repeats every 24 hours
  private func setupDailyTimer() {
    todayUpdateTimer = Timer.scheduledTimer(
      withTimeInterval: 24 * 60 * 60,
      repeats: true
    ) { [weak self] _ in
      self?.updateTodayDate()
    }
  }

  /// Update today date and refresh visible cells
  private func updateTodayDate() {
    let oldToday = todayDate
    todayDate = self.config.calendar.startOfDay(for: Date())

    // Only refresh if date actually changed
    if !self.config.calendar.isDate(oldToday, inSameDayAs: todayDate) {
      DispatchQueue.main.async { [weak self] in
        // No need to clear lunar date cache - only affects UI styling
        self?.calendarView.reloadData()
      }
    }
  }

  /// Cleanup today timer
  private func cleanupTodayTimer() {
    todayUpdateTimer?.invalidate()
    todayUpdateTimer = nil

    // Remove notification observer
    NotificationCenter.default.removeObserver(
      self,
      name: UIApplication.didBecomeActiveNotification,
      object: nil
    )
  }

  /// Simple cache for lunar date calculations only
  private func getCachedLunarDate(for date: Date) -> (day: Int, month: Int)? {
    let key = lunarCacheKey(for: date)
    if let mem = lunarDateCache[key] {
      return mem
    }
    if let persisted = LunarPersistentCache.shared.get(key) {
      lunarDateCache[key] = persisted
      return persisted
    }
    return nil
  }

  private func setCachedLunarDate(
    _ lunarDate: (day: Int, month: Int),
    for date: Date
  ) {
    let key = lunarCacheKey(for: date)
    lunarDateCache[key] = lunarDate
    LunarPersistentCache.shared.set(lunarDate, for: key)

    // Simple size limit - remove oldest entries if cache gets too large
    if lunarDateCache.count > 1000 {
      let sortedKeys = lunarDateCache.keys.sorted()
      let keysToRemove = sortedKeys.prefix(200)  // Remove oldest 200 entries
      for key in keysToRemove {
        lunarDateCache.removeValue(forKey: key)
      }
    }
  }

  private func lunarCacheKey(for date: Date) -> String {
    let start = calendar.startOfDay(for: date)
    let ts = start.timeIntervalSince1970
    let tz = self.config.calendar.timeZone.secondsFromGMT(for: start)
    return String(format: "%.0f.tz.%d", ts, tz)
  }

  // MARK: - Private Methods

  private func configureUI() {
    self.view.backgroundColor = self.config.controller.backgroundColor
      .toUIColor()
    self.navigationItem.largeTitleDisplayMode = .never
    self.navigationItem.titleView = self.titleUI
    self.navigationItem.leftBarButtonItem = self.cancelBarButtonItem

    // Keep default navigation bar and title; no special handling needed when hiding submit button
  }

  private func configureSubviews() {
    self.calendarView.register(
      DayCell.self,
      forCellWithReuseIdentifier: self.dayCellReuseIdentifier
    )
    self.calendarView.register(
      MonthHeader.self,
      forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
      withReuseIdentifier: self.monthHeaderReuseIdentifier
    )

    // Only add top header if showHeader is true
    if self.config.controller.showHeader {
      self.view.addSubview(self.topHeaderContainer)
      self.topHeaderContainer.layer.insertSublayer(
        self.topHeaderGradientLayer,
        at: 0
      )
      self.topHeaderGradientLayer.colors = self.config.controller
        .confirmGradientColors?.map { $0.toUIColor().cgColor }

      self.topHeaderContainer.addSubview(self.departTitleLabel)
      self.topHeaderContainer.addSubview(self.departDateLabel)
      self.topHeaderContainer.addSubview(self.leftDecorImageView)
      if !self.config.controller.isSingleMode {
        self.topHeaderContainer.addSubview(self.topHeaderDivider)
        self.topHeaderContainer.addSubview(self.returnTitleLabel)
        self.topHeaderContainer.addSubview(self.returnDateLabel)
        self.topHeaderContainer.addSubview(self.rightDecorImageView)
      }
    }

    self.view.addSubview(self.weekView)
    self.view.addSubview(self.calendarView)
    if self.config.controller.showSubmitButton {
      self.view.addSubview(self.bottomContainer)
      self.bottomContainer.addSubview(self.bottomTopBorder)
      self.bottomContainer.addSubview(self.confirmButton)
      self.confirmButton.layer.insertSublayer(self.confirmGradientLayer, at: 0)
      // Apply gradient colors from config (when button is visible)
      self.confirmGradientLayer.colors = self.config.controller
        .confirmGradientColors?.map { $0.toUIColor().cgColor }
    }
  }

  private func configureConstraints() {
    // Only set up top header constraints if showHeader is true
    if self.config.controller.showHeader {
      NSLayoutConstraint.activate([
        self.topHeaderContainer.topAnchor.constraint(
          equalTo: self.view.safeAreaLayoutGuide.topAnchor
        ),
        self.topHeaderContainer.leftAnchor.constraint(
          equalTo: self.view.leftAnchor
        ),
        self.topHeaderContainer.rightAnchor.constraint(
          equalTo: self.view.rightAnchor
        ),
      ])

      if self.config.controller.isSingleMode {
        NSLayoutConstraint.activate([
          self.departTitleLabel.topAnchor.constraint(
            equalTo: self.topHeaderContainer.topAnchor,
            constant: Constants.Layout.topHeaderVerticalPadding
          ),
          self.departTitleLabel.centerXAnchor.constraint(
            equalTo: self.topHeaderContainer.centerXAnchor
          ),

          self.departDateLabel.topAnchor.constraint(
            equalTo: self.departTitleLabel.bottomAnchor,
            constant: Constants.Layout.topHeaderVerticalPadding
          ),
          self.departDateLabel.centerXAnchor.constraint(
            equalTo: self.topHeaderContainer.centerXAnchor
          ),
          self.departDateLabel.bottomAnchor.constraint(
            equalTo: self.topHeaderContainer.bottomAnchor,
            constant: -Constants.Layout.topHeaderVerticalPadding
          ),
        ])
      } else {
        // Layout internal labels in header (centered per half)
        let leftGuide = UILayoutGuide()
        let rightGuide = UILayoutGuide()
        self.topHeaderContainer.addLayoutGuide(leftGuide)
        self.topHeaderContainer.addLayoutGuide(rightGuide)

        NSLayoutConstraint.activate([
          leftGuide.leadingAnchor.constraint(
            equalTo: self.topHeaderContainer.leadingAnchor
          ),
          leftGuide.trailingAnchor.constraint(
            equalTo: self.topHeaderDivider.leadingAnchor
          ),
          leftGuide.topAnchor.constraint(
            equalTo: self.topHeaderContainer.topAnchor
          ),
          leftGuide.bottomAnchor.constraint(
            equalTo: self.topHeaderContainer.bottomAnchor
          ),

          rightGuide.leadingAnchor.constraint(
            equalTo: self.topHeaderDivider.trailingAnchor
          ),
          rightGuide.trailingAnchor.constraint(
            equalTo: self.topHeaderContainer.trailingAnchor
          ),
          rightGuide.topAnchor.constraint(
            equalTo: self.topHeaderContainer.topAnchor
          ),
          rightGuide.bottomAnchor.constraint(
            equalTo: self.topHeaderContainer.bottomAnchor
          ),
        ])

        NSLayoutConstraint.activate([
          self.departTitleLabel.topAnchor.constraint(
            equalTo: self.topHeaderContainer.topAnchor,
            constant: Constants.Layout.topHeaderVerticalPadding
          ),
          self.departTitleLabel.centerXAnchor.constraint(
            equalTo: leftGuide.centerXAnchor
          ),

          self.departDateLabel.topAnchor.constraint(
            equalTo: self.departTitleLabel.bottomAnchor,
            constant: Constants.Layout.topHeaderVerticalPadding
          ),
          self.departDateLabel.centerXAnchor.constraint(
            equalTo: leftGuide.centerXAnchor
          ),
          self.departDateLabel.bottomAnchor.constraint(
            equalTo: self.topHeaderContainer.bottomAnchor,
            constant: -Constants.Layout.topHeaderVerticalPadding
          ),

          self.topHeaderDivider.centerXAnchor.constraint(
            equalTo: self.topHeaderContainer.centerXAnchor
          ),
          self.topHeaderDivider.topAnchor.constraint(
            equalTo: self.topHeaderContainer.topAnchor,
            constant: Constants.Layout.topHeaderVerticalPadding
          ),
          self.topHeaderDivider.bottomAnchor.constraint(
            equalTo: self.topHeaderContainer.bottomAnchor,
            constant: -Constants.Layout.topHeaderVerticalPadding
          ),
          self.topHeaderDivider.widthAnchor.constraint(equalToConstant: Constants.Layout.topHeaderDividerThickness),

          self.returnTitleLabel.topAnchor.constraint(
            equalTo: self.topHeaderContainer.topAnchor,
            constant: Constants.Layout.topHeaderVerticalPadding
          ),
          self.returnTitleLabel.centerXAnchor.constraint(
            equalTo: rightGuide.centerXAnchor
          ),

          self.returnDateLabel.topAnchor.constraint(
            equalTo: self.returnTitleLabel.bottomAnchor,
            constant: Constants.Layout.topHeaderVerticalPadding
          ),
          self.returnDateLabel.centerXAnchor.constraint(
            equalTo: rightGuide.centerXAnchor
          ),
          self.returnDateLabel.bottomAnchor.constraint(
            equalTo: self.topHeaderContainer.bottomAnchor,
            constant: -Constants.Layout.topHeaderVerticalPadding
          ),
        ])
      }

      // Decorative images constraints (absolute positions)
      // from image (left) is used in both modes
      NSLayoutConstraint.activate([
        self.leftDecorImageView.leftAnchor.constraint(
          equalTo: self.topHeaderContainer.leftAnchor,
          constant: 0
        ),
        self.leftDecorImageView.topAnchor.constraint(
          equalTo: self.topHeaderContainer.topAnchor,
          constant: 0
        ),
        self.leftDecorImageView.widthAnchor.constraint(equalToConstant: 40),
        self.leftDecorImageView.heightAnchor.constraint(equalToConstant: 40),
      ])

      // to image (right) is ONLY used in range mode
      if !self.config.controller.isSingleMode {
        NSLayoutConstraint.activate([
          self.rightDecorImageView.rightAnchor.constraint(
            equalTo: self.topHeaderContainer.rightAnchor,
            constant: 0
          ),
          self.rightDecorImageView.topAnchor.constraint(
            equalTo: self.topHeaderContainer.topAnchor,
            constant: 0
          ),
          self.rightDecorImageView.widthAnchor.constraint(equalToConstant: 40),
          self.rightDecorImageView.heightAnchor.constraint(equalToConstant: 40),
        ])
      }
    }

    // Set up week view constraints based on whether top header is shown
    if self.config.controller.showHeader {
      NSLayoutConstraint.activate([
        self.weekView.topAnchor.constraint(
          equalTo: self.topHeaderContainer.bottomAnchor
        ),
        self.weekView.leftAnchor.constraint(
          equalTo: self.view.leftAnchor,
          constant: Constants.Layout.weekViewSidePadding
        ),
        self.weekView.rightAnchor.constraint(
          equalTo: self.view.rightAnchor,
          constant: -Constants.Layout.weekViewSidePadding
        ),
      ])
    } else {
      NSLayoutConstraint.activate([
        self.weekView.topAnchor.constraint(
          equalTo: self.view.safeAreaLayoutGuide.topAnchor
        ),
        self.weekView.leftAnchor.constraint(
          equalTo: self.view.leftAnchor,
          constant: Constants.Layout.weekViewSidePadding
        ),
        self.weekView.rightAnchor.constraint(
          equalTo: self.view.rightAnchor,
          constant: -Constants.Layout.weekViewSidePadding
        ),
      ])
    }

    NSLayoutConstraint.activate([
      self.calendarView.topAnchor.constraint(
        equalTo: self.weekView.bottomAnchor
      ),
      self.calendarView.leftAnchor.constraint(
        equalTo: self.view.leftAnchor,
        constant: Constants.Layout.calendarHorizontalPadding
      ),
      self.calendarView.rightAnchor.constraint(
        equalTo: self.view.rightAnchor,
        constant: -Constants.Layout.calendarHorizontalPadding
      ),
    ])

    if self.config.controller.showSubmitButton {
      NSLayoutConstraint.activate([
        self.bottomContainer.leftAnchor.constraint(equalTo: self.view.leftAnchor),
        self.bottomContainer.rightAnchor.constraint(
          equalTo: self.view.rightAnchor
        ),
        self.bottomContainer.bottomAnchor.constraint(
          equalTo: self.view.safeAreaLayoutGuide.bottomAnchor
        ),
      ])

      NSLayoutConstraint.activate([
        self.bottomTopBorder.topAnchor.constraint(
          equalTo: self.bottomContainer.topAnchor
        ),
        self.bottomTopBorder.leftAnchor.constraint(
          equalTo: self.bottomContainer.leftAnchor
        ),
        self.bottomTopBorder.rightAnchor.constraint(
          equalTo: self.bottomContainer.rightAnchor
        ),
        self.bottomTopBorder.heightAnchor.constraint(equalToConstant: 1.0),
      ])

      NSLayoutConstraint.activate([
        self.confirmButton.leftAnchor.constraint(
          equalTo: self.bottomContainer.leftAnchor,
          constant: Constants.Layout.confirmButtonHorizontalMargin
        ),
        self.confirmButton.rightAnchor.constraint(
          equalTo: self.bottomContainer.rightAnchor,
          constant: -Constants.Layout.confirmButtonHorizontalMargin
        ),
        self.confirmButton.topAnchor.constraint(
          equalTo: self.bottomContainer.topAnchor,
          constant: Constants.Layout.confirmButtonTopMargin
        ),
        self.confirmButton.bottomAnchor.constraint(
          equalTo: self.bottomContainer.bottomAnchor,
          constant: -Constants.Layout.confirmButtonBottomMargin
        ),
      ])

      NSLayoutConstraint.activate([
        self.calendarView.bottomAnchor.constraint(
          equalTo: self.bottomContainer.topAnchor
        )
      ])
    } else {
      NSLayoutConstraint.activate([
        self.calendarView.bottomAnchor.constraint(
          equalTo: self.view.bottomAnchor
        )
      ])
    }
  }

  private func setupCalendarContentInsets() {
    let bottomInset: CGFloat = self.config.controller.showSubmitButton ? 20 : self.view.safeAreaInsets.bottom
    self.calendarView.contentInset.bottom = bottomInset
    self.calendarView.verticalScrollIndicatorInsets.bottom = bottomInset
  }

  private func configureInitialState() {
    self.value = self.initialValue
    if let rangeValue = self.value as? PickerRange {
      self.selectRange(rangeValue, in: self.calendarView)
      // In single mode, enable confirm when from is selected even if to is nil
      if self.config.controller.isSingleMode {
        setConfirmButtonEnabled(true)
      } else {
        // Range mode: only enable when toDate exists
        setConfirmButtonEnabled(rangeValue.toDate != nil)
      }
      updateTopHeader(with: rangeValue)
    } else {
      setConfirmButtonEnabled(false)
      updateTopHeader(with: nil)
    }
    // Set flag to perform scroll in viewDidLayoutSubviews when sizes are ready
    self.needsInitialScroll = true
  }

  internal func configureCell(
    _ cell: JTACDayCell,
    forItemAt date: Date,
    cellState: CellState,
    indexPath: IndexPath
  ) {
    guard let cell = cell as? DayCell else { return }

    cell.applyConfig(self.config)

    // Always create fresh config to avoid UI state cache issues
    // Only lunar date calculation is cached separately
    let newConfig = createCellConfig(for: cellState, date: date)
    cell.configure(for: newConfig)
  }

  /// Creates cell configuration with optimized price lookup
  private func createCellConfig(for cellState: CellState, date: Date)
    -> DayCell.ViewConfig
  {
    var config = DayCell.makeViewConfig(
      for: cellState,
      minimumDate: self.privateMinimumDate,
      maximumDate: self.privateMaximumDate,
      rangeValue: self.value as? PickerRange,
      calendar: self.config.calendar
    )

    // Only configure content for dates that belong to current month
    if cellState.dateBelongsTo == .thisMonth {
      // Configure date label
      if config.dateLabelText != nil {
        config.dateLabelText = self.dayFormatter.string(from: date)
      }

      // Check if this is today
      let isToday = self.config.calendar.isDate(date, inSameDayAs: todayDate)

      // Configure date label color and font weight - today gets today color and medium weight
      if isToday {
        config.dateLabelColor = self.config.dayCell.todayLabelColor
        config.dateLabelFontWeight = UIFont.Weight.medium
      } else {
        config.dateLabelColor =
          self.config.calendar.isDateInWeekend(date)
          ? self.config.dayCell.weekendLabelColor
          : self.config.dayCell.dateLabelColor
        config.dateLabelFontWeight = UIFont.Weight.regular
      }

      // Configure lunar date
      configureLunarDate(for: &config, date: date)

    }

    return config
  }

  /// Configures lunar date information for the cell with caching
  private func configureLunarDate(
    for config: inout DayCell.ViewConfig,
    date: Date
  ) {
    // Try to get from cache first
    let lunarDate: (day: Int, month: Int)
    if let cached = getCachedLunarDate(for: date) {
      lunarDate = cached
    } else {
      // Calculate and cache the result
      let calculated = getVietnameseLunarDate(
        date,
        self.config.calendar.timeZone
      )
      lunarDate = (day: calculated.day, month: calculated.month)
      setCachedLunarDate(lunarDate, for: date)
    }

    config.lunarDateLabelText =
      lunarDate.day == 1
      ? "\(lunarDate.day)/\(lunarDate.month)"
      : "\(lunarDate.day)"

    config.lunarDateLabelColor =
      lunarDate.day == 1
      ? self.config.dayCell.specialDateLabelColor
      : self.config.dayCell.lunarDateLabelColor
  }

  // MARK: - Actions

  @objc
  private func cancel() {
    // Notify cancel handler
    self.cancelHandler?()
    // Dismiss - cleanup will be handled automatically by deinit
    self.navigationController?.dismiss(animated: true, completion: nil)
  }

  @objc
  private func done() {
    self.doneHandler?(self.value)
    // Dismiss - cleanup will be handled automatically by deinit
    self.navigationController?.dismiss(animated: true, completion: nil)
  }

  @objc
  private func confirmTapped() {
    self.done()
  }

  private func selectValue(_ value: Value?, in calendar: JTACMonthView) {
    if let range = value as? PickerRange {
      self.selectRange(range, in: calendar)
    }
  }

  // MARK: - Range Selection Logic

  internal func handleDateTap(in calendar: JTACMonthView, date: Date) {
    // Auto-submit short-circuit
    if isAutoSubmit {
      if handleAutoSubmit(date: date, in: calendar) { return }
    }

    // Default behavior when submit button is shown
    if self.config.controller.isSingleMode {
      // Single mode: always set from to selected date, to = nil
      let from = date.startOfDay(in: self.config.calendar)
      let newRange = PickerRange(from: from, to: nil)
      self.value = newRange as? Value
      calendar.deselectAllDates(triggerSelectionDelegate: false)
      DispatchQueue.main.async { [weak self] in
        calendar.reloadData()
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
          self?.selectValue(newRange as? Value, in: calendar)
        }
      }
      setConfirmButtonEnabled(true)
      updateTopHeader(with: newRange)
      return
    }
    let dateHelper = DateRangeHelper(calendar: self.config.calendar)
    let currentRange = self.value as? PickerRange
    let newRange = dateHelper.calculateNewRange(
      currentRange: currentRange,
      selectedDate: date,
      hasInitialValue: self.initialValue != nil
    )

    // Check if this is a new from date selection
    // This happens when:
    // 1. No current range (first selection)
    // 2. Current range exists but we're starting a new range (different from date)
    let isNewFromDateSelection: Bool = {
      if newRange.shouldComplete { return false }
      if let prevFrom = currentRange?.fromDate {
        return !newRange.range.fromDate.isInSameDay(
          in: self.config.calendar,
          date: prevFrom
        )
      }
      return true
    }()

    if newRange.shouldComplete {
      // Range is complete - update value and enable confirm
      self.value = newRange.range as? Value

      // THÊM DÒNG NÀY: Cập nhật UI để hiển thị range selection
      self.selectValue(newRange.range as? Value, in: calendar)

      setConfirmButtonEnabled(true)
      updateTopHeader(with: newRange.range)
    } else {
      // Range not complete - update UI for continued selection
      if isNewFromDateSelection {
        // Clear all selections first to avoid visual conflicts
        calendar.deselectAllDates(triggerSelectionDelegate: false)
        // No need to clear cache - UI state is not cached
        // Force reload and then update value + selection
        DispatchQueue.main.async { [weak self] in
          calendar.reloadData()
          // Update value after reload to ensure UI sync
          self?.value = newRange.range as? Value
          // Small delay to ensure reload completes before selection
          DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
            self?.selectValue(newRange.range as? Value, in: calendar)
          }
        }
      } else {
        // Normal case - update value immediately
        self.value = newRange.range as? Value
        self.selectValue(newRange.range as? Value, in: calendar)
      }
      setConfirmButtonEnabled(false)
    }
  }

  // MARK: - Auto Submit Helpers

  /// Handles auto-submit tap. Returns true if the tap was fully handled (including dismissal)
  private func handleAutoSubmit(date: Date, in calendar: JTACMonthView) -> Bool {
    if self.config.controller.isSingleMode {
      let from = date.startOfDay(in: self.config.calendar)
      let newRange = PickerRange(from: from, to: nil)
      self.value = newRange as? Value
      self.done()
      return true
    }

    guard let current = (self.value as? PickerRange) else {
      // Start new selection
      let from = date.startOfDay(in: self.config.calendar)
      let newRange = PickerRange(from: from, to: nil)
      self.value = newRange as? Value
      calendar.deselectAllDates(triggerSelectionDelegate: false)
      DispatchQueue.main.async { [weak self] in
        calendar.reloadData()
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
          self?.selectValue(newRange as? Value, in: calendar)
        }
      }
      updateTopHeader(with: newRange)
      return true
    }

    if current.toDate != nil {
      // Start a new range from tapped date
      let from = date.startOfDay(in: self.config.calendar)
      let newRange = PickerRange(from: from, to: nil)
      self.value = newRange as? Value
      calendar.deselectAllDates(triggerSelectionDelegate: false)
      DispatchQueue.main.async { [weak self] in
        calendar.reloadData()
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
          self?.selectValue(newRange as? Value, in: calendar)
        }
      }
      updateTopHeader(with: newRange)
      return true
    }

    let fromDate = current.fromDate
    if date < fromDate {
      // Reset from and continue
      let newFrom = date.startOfDay(in: self.config.calendar)
      let newRange = PickerRange(from: newFrom, to: nil)
      self.value = newRange as? Value
      calendar.deselectAllDates(triggerSelectionDelegate: false)
      DispatchQueue.main.async { [weak self] in
        calendar.reloadData()
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
          self?.selectValue(newRange as? Value, in: calendar)
        }
      }
      updateTopHeader(with: newRange)
      return true
    } else {
      // Complete and close
      let completed = PickerRange(
        from: fromDate,
        to: date.endOfDay(in: self.config.calendar)
      )
      self.value = completed as? Value
      self.done()
      return true
    }
  }

  private func selectRange(_ range: PickerRange, in calendar: JTACMonthView) {
    calendar.deselectAllDates(triggerSelectionDelegate: false)
    calendar.selectDates(
      from: range.fromDate,
      to: range.toDate ?? range.fromDate,
      triggerSelectionDelegate: true,
      keepSelectionIfMultiSelectionAllowed: false
    )
    // Ensure layout happens before we compute round corners in cells
    calendar.layoutIfNeeded()
    // Remove reloadItems() call as it overrides the selection state set by selectDates()
    // The triggerSelectionDelegate: true above will properly configure the cells
    updateTopHeader(with: range)
  }

  // Note: UI state cache removed - only lunar date calculations are cached
  // This eliminates all cache-related UI bugs while maintaining performance for expensive lunar calculations

  @objc
  private func handleAppBecameActive() {
    updateTodayDate()
  }

  private func setConfirmButtonEnabled(_ enabled: Bool) {
    guard self.config.controller.showSubmitButton else { return }
    self.confirmButton.isEnabled = enabled
    self.confirmButton.alpha = enabled ? 1.0 : 0.7
  }

  private func updateTopHeader(with range: PickerRange?) {
    // Only update header if showHeader is true
    guard self.config.controller.showHeader else { return }
    
    let tz = self.config.calendar.timeZone
    let formatter = DateFormatter()
    formatter.locale = self.config.calendar.locale
    formatter.timeZone = tz
    formatter.dateFormat = "EEEE, dd MMMM yyyy"

    let defaultNotSelectedText = self.config.controller.notSelectedText ?? "Chưa chọn"

    if let range = range {
      self.departDateLabel.text = formatter.string(from: range.fromDate)
      if !self.config.controller.isSingleMode {
        if let toDate = range.toDate {
          self.returnDateLabel.text = formatter.string(from: toDate)
        } else {
          self.returnDateLabel.text = defaultNotSelectedText
        }
      }
    } else {
      self.departDateLabel.text = defaultNotSelectedText
      if !self.config.controller.isSingleMode {
        self.returnDateLabel.text = defaultNotSelectedText
      }
    }
  }

  override public func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if self.config.controller.showSubmitButton {
      self.confirmGradientLayer.frame = self.confirmButton.bounds
      self.confirmGradientLayer.cornerRadius =
        self.confirmButton.layer.cornerRadius
      self.confirmGradientLayer.masksToBounds = true
    }

    // Only update top header layout if showHeader is true
    if self.config.controller.showHeader {
      self.topHeaderGradientLayer.frame = self.topHeaderContainer.bounds
      self.topHeaderContainer.setNeedsLayout()
      self.topHeaderContainer.layoutIfNeeded()
    }

    // Update bottom padding so the last content isn't obscured by the home indicator
    let desiredBottomInset: CGFloat = self.config.controller.showSubmitButton ? 20 : self.view.safeAreaInsets.bottom
    self.calendarView.contentInset.bottom = desiredBottomInset
    self.calendarView.verticalScrollIndicatorInsets.bottom = desiredBottomInset

    if needsInitialScroll {
      needsInitialScroll = false
      // Ensure layout is finished so contentSize is accurate for scrolling to the end
      self.calendarView.layoutIfNeeded()

      if let rangeValue = self.value as? PickerRange {
        let fromDate = rangeValue.fromDate
        self.calendarView.scrollToHeaderForDate(fromDate)
      } else {
        let nowDate = Date()
        let targetDate = self.privateMaximumDate ?? nowDate
        let scrollDate = targetDate < nowDate ? targetDate : nowDate
        self.calendarView.scrollToHeaderForDate(scrollDate)
      }
    }
  }

  // MARK: - Cache Prewarm

  /// Pre-calculate lunar dates for a range of months and persist them to the on-disk cache.
  /// This runs on a background queue and does not modify the in-memory cache to avoid contention.
  private func prewarmLunarCache(monthsBefore: Int, monthsAfter: Int) {
    let calendar = self.config.calendar
    let timeZone = calendar.timeZone
    let now = Date()

    DispatchQueue.global(qos: .utility).async { [weak self] in
      guard let self = self else { return }

      let startOffset = -abs(monthsBefore)
      let endOffset = abs(monthsAfter)

      if startOffset > endOffset { return }

      for offset in startOffset...endOffset {
        guard
          let monthDate = calendar.date(
            byAdding: .month,
            value: offset,
            to: now
          )
        else { continue }
        let startOfMonth = monthDate.startOfMonth(in: calendar)
        let endOfMonth = monthDate.endOfMonth(in: calendar)

        var cursor = startOfMonth
        while cursor <= endOfMonth {
          let key = self.lunarCacheKey(for: cursor)
          if LunarPersistentCache.shared.get(key) == nil {
            let ld = getVietnameseLunarDate(cursor, timeZone)
            LunarPersistentCache.shared.set((ld.day, ld.month), for: key)
          }
          guard let next = calendar.date(byAdding: .day, value: 1, to: cursor)
          else { break }
          cursor = next
        }
      }
    }
  }
}

// MARK: - Configuration Extension

extension PickerConfig {
  public struct PickerController {
    public var title = ""
    public var cancelColor = ColorWrapper.systemBlue
    public var titleColor = ColorWrapper.customBlack
    public var backgroundColor = ColorWrapper.customWhite
    public var confirmGradientColors: [ColorWrapper]? = nil
    public var secondaryTextColor: ColorWrapper = ColorWrapper.customBlack
    public var fromText: String? = "Ngày đi"
    public var toText: String? = "Ngày về"
    public var submitText: String = "Xác nhận"
    public var notSelectedText: String? = "Chưa chọn"
    public var showHeader: Bool = true
    public var borderColor: ColorWrapper = ColorWrapper.systemBlue
    public var isSingleMode: Bool = false
    public var showSubmitButton: Bool = true
    public var fromImage: LDP_NativeAssetSource = LDP_NativeAssetSource(uri: "", width: nil, height: nil, scale: nil)
    public var toImage: LDP_NativeAssetSource = LDP_NativeAssetSource(uri: "", width: nil, height: nil, scale: nil)
    public var closeImage: LDP_NativeAssetSource = LDP_NativeAssetSource(uri: "", width: nil, height: nil, scale: nil)
  }
}

// MARK: - Helper Classes

/// Helper class to manage date range selection logic
private struct DateRangeHelper {
  let calendar: Calendar

  struct RangeResult {
    let range: PickerRange
    let shouldComplete: Bool
  }

  func calculateNewRange(
    currentRange: PickerRange?,
    selectedDate: Date,
    hasInitialValue: Bool
  ) -> RangeResult {

    guard let currentValue = currentRange else {
      return RangeResult(
        range: .from(
          selectedDate.startOfDay(in: calendar),
          to: nil
        ),
        shouldComplete: false
      )
    }

    // Range is complete if toDate is not nil
    let rangeSelected = currentValue.toDate != nil

    // Handle initial value case
    if !rangeSelected && hasInitialValue {
      return handleInitialValueCase(
        currentValue: currentValue,
        selectedDate: selectedDate
      )
    }

    // Handle range already selected case (including completed same-day range)
    if rangeSelected {
      // Begin a new selection from the newly selected date (incomplete)
      return RangeResult(
        range: .from(
          selectedDate.startOfDay(in: calendar),
          to: nil
        ),
        shouldComplete: false
      )
    }

    // Simple rule: if selectedDate < from -> swap (new from = selected, to = old from)
    if selectedDate < currentValue.fromDate {
      return RangeResult(
        range: .from(
          selectedDate.startOfDay(in: calendar),
          to: currentValue.fromDate.endOfDay(in: calendar)
        ),
        shouldComplete: true
      )
    }

    // Otherwise set toDate to selected (complete), even if same-day
    return RangeResult(
      range: .from(
        currentValue.fromDate,
        to: selectedDate.endOfDay(in: calendar)
      ),
      shouldComplete: true
    )
  }

  private func handleInitialValueCase(
    currentValue: PickerRange,
    selectedDate: Date
  ) -> RangeResult {
    if selectedDate < currentValue.fromDate {
      return RangeResult(
        range: .from(
          selectedDate.startOfDay(in: calendar),
          to: nil
        ),
        shouldComplete: false
      )
    } else {
      return RangeResult(
        range: .from(
          currentValue.fromDate,
          to: selectedDate.endOfDay(in: calendar)
        ),
        shouldComplete: true
      )
    }
  }


}
