import Combine
import DesignSystem
import Env
import Models
import NetworkClient
import NukeUI
import SFSafeSymbols
import SwiftData
import SwiftUI

@MainActor
struct EditTagGroupView: View {
  @Environment(\.dismiss) private var dismiss
  @Environment(\.modelContext) private var context
  @Environment(Theme.self) private var theme

  @State private var tagGroup: TagGroup

  private let onSaved: ((TagGroup) -> Void)?
  private let isNewGroup: Bool

  @FocusState private var focusedField: Focus?

  var body: some View {
    NavigationStack {
      Form {
        Section {
          TitleInputView(
            title: $tagGroup.title,
            titleValidationStatus: tagGroup.titleValidationStatus,
            focusedField: $focusedField,
            isNewGroup: isNewGroup
          )

          SymbolInputView(
            selectedSymbol: $tagGroup.symbolName,
            selectedSymbolValidationStatus: tagGroup.symbolNameValidationStatus,
            focusedField: $focusedField
          )
        }
        #if !os(visionOS)
          .listRowBackground(theme.primaryBackgroundColor)
        #endif

        Section("add-tag-groups.edit.tags") {
          TagsInputView(
            tags: $tagGroup.tags,
            tagsValidationStatus: tagGroup.tagsValidationStatus,
            focusedField: $focusedField
          )
        }
        #if !os(visionOS)
          .listRowBackground(theme.primaryBackgroundColor)
        #endif
      }
      .formStyle(.grouped)
      .navigationTitle(
        isNewGroup
          ? "timeline.filter.add-tag-groups"
          : "timeline.filter.edit-tag-groups"
      )
      .navigationBarTitleDisplayMode(.inline)
      #if !os(visionOS)
        .scrollContentBackground(.hidden)
        .background(theme.secondaryBackgroundColor)
        .scrollDismissesKeyboard(.interactively)
      #endif
      .toolbar {
        CancelToolbarItem()
        ToolbarItem(placement: .navigationBarTrailing) {
          Button("action.save", action: { save() })
            .disabled(!tagGroup.isValid)
        }
      }
      .onAppear {
        focusedField = .title
      }
    }
  }

  init(tagGroup: TagGroup = .emptyGroup(), onSaved: ((TagGroup) -> Void)? = nil) {
    _tagGroup = State(wrappedValue: tagGroup)
    self.onSaved = onSaved
    isNewGroup = tagGroup.title.isEmpty
  }

  private func save() {
    tagGroup.format()
    context.insert(tagGroup)
    onSaved?(tagGroup)

    dismiss()
  }

  enum Focus {
    case title
    case symbol
    case new
  }
}

struct AddTagGroupView_Previews: PreviewProvider {
  static var previews: some View {
    let container = try? ModelContainer(for: TagGroup.self, configurations: ModelConfiguration())

    // need to use `sheet` to show `symbolsSuggestionView` in preview
    return Text(verbatim: "parent view for EditTagGroupView")
      .sheet(isPresented: .constant(true)) {
        EditTagGroupView()
          .withEnvironments()
          .modelContainer(container!)
      }
  }
}

private struct TitleInputView: View {
  @Binding var title: String
  let titleValidationStatus: TagGroup.TitleValidationStatus

  @FocusState.Binding var focusedField: EditTagGroupView.Focus?

  @Query var tagGroups: [TagGroup]

  let isNewGroup: Bool

  var body: some View {
    VStack(alignment: .leading) {
      TextField("add-tag-groups.edit.title.field", text: $title, axis: .horizontal)
        .focused($focusedField, equals: .title)
        .onSubmit {
          focusedField = .symbol
        }

      if focusedField == .title, warningText != "" {
        Text(warningText).warningLabel()
      }
    }
  }

  var warningText: LocalizedStringKey {
    if case let .invalid(description) = titleValidationStatus {
      return description
    } else if isNewGroup,
      tagGroups.contains(where: { $0.title == title })
    {
      return "\(title) add-tag-groups.edit.title.field.warning.already-exists"
    }
    return ""
  }
}

private struct SymbolInputView: View {
  @State private var symbolQuery = ""

  @Binding var selectedSymbol: String
  let selectedSymbolValidationStatus: TagGroup.SymbolNameValidationStatus

  @FocusState.Binding var focusedField: EditTagGroupView.Focus?

  var body: some View {
    VStack(alignment: .leading) {
      HStack {
        TextField("add-tag-groups.edit.icon.field", text: $symbolQuery, axis: .horizontal)
          .textInputAutocapitalization(.never)
          .autocorrectionDisabled()
          .focused($focusedField, equals: .symbol)
          .onSubmit {
            if TagGroup.allSymbols.contains(symbolQuery) {
              selectedSymbol = symbolQuery
            }
            focusedField = .new
          }
          .onChange(of: focusedField) {
            symbolQuery = selectedSymbol
          }

        Image(systemName: selectedSymbol)
          .frame(height: 30)
      }

      if case let .invalid(description) = selectedSymbolValidationStatus,
        focusedField == .symbol
      {
        Text(description).warningLabel()
      }

      if focusedField == .symbol {
        SymbolSearchResultsView(
          symbolQuery: $symbolQuery,
          selectedSymbol: $selectedSymbol,
          focusedField: $focusedField
        )
        .frame(maxWidth: .infinity, alignment: .leading)
        .frame(height: 40)
      }
    }
  }
}

private struct TagsInputView: View {
  @State private var newTag: String = ""
  @Binding var tags: [String]
  let tagsValidationStatus: TagGroup.TagsValidationStatus

  @FocusState.Binding var focusedField: EditTagGroupView.Focus?

  var body: some View {
    var warningText: LocalizedStringKey {
      if tags.contains(newTag) {
        return "add-tag-groups.edit.tags.field.warning.duplicated-tag"
      } else if case let .invalid(description) = tagsValidationStatus {
        return description
      }
      return ""
    }

    ForEach(tags, id: \.self) { tag in
      HStack {
        Text(tag)
        Spacer()
        Button {
          deleteTag(tag)
        } label: {
          Image(systemName: "trash")
            .foregroundStyle(.red)
        }
        .buttonStyle(.plain)
      }
    }
    .onDelete { indexes in
      if let index = indexes.first {
        let tag = tags[index]
        deleteTag(tag)
      }
    }

    // this `VStack` need to be here to overcome a SwiftUI bug
    // "add new tag" `TextField` is not focused after adding the first tag
    VStack(alignment: .leading) {
      HStack {
        // this condition is using to overcome a SwiftUI bug
        // "add new tag" `TextField` is not focused after adding the first tag
        if tags.isEmpty {
          addNewTagTextField()
        } else {
          addNewTagTextField()
            .onAppear { focusedField = .new }
        }

        Spacer()

        if !newTag.isEmpty, !tags.contains(newTag) {
          Button {
            addNewTag()
          } label: {
            Image(systemName: "checkmark.circle.fill").tint(.green)
          }
        }
      }

      if focusedField == .new, warningText != "" {
        Text(warningText).warningLabel()
      }
    }
  }

  private func addNewTagTextField() -> some View {
    VStack(alignment: .leading) {
      TextField("add-tag-groups.edit.tags.add", text: $newTag, axis: .horizontal)
        .textInputAutocapitalization(.never)
        .autocorrectionDisabled()
        .onSubmit {
          addNewTag()
        }
        .focused($focusedField, equals: .new)
    }
  }

  private func addNewTag() {
    addTag(newTag.trimmingCharacters(in: .whitespaces).lowercased())
    newTag = ""
    focusedField = .new
  }

  private func addTag(_ tag: String) {
    guard !tag.isEmpty,
      !tags.contains(tag)
    else { return }

    tags.append(tag)
    tags.sort()
  }

  private func deleteTag(_ tag: String) {
    tags.removeAll(where: { $0 == tag })
  }
}

private struct SymbolSearchResultsView: View {
  @Binding var symbolQuery: String
  @Binding var selectedSymbol: String
  @State private var results: [String] = []

  @FocusState.Binding var focusedField: EditTagGroupView.Focus?

  var body: some View {
    Group {
      switch validationStatus {
      case .valid:
        ScrollView(.horizontal, showsIndicators: false) {
          LazyHStack {
            ForEach(results, id: \.self) { name in
              Button {
                results = TagGroup.searchSymbol(for: symbolQuery, exclude: selectedSymbol)
                selectedSymbol = name
                symbolQuery = name
                focusedField = .new
              } label: {
                Image(systemName: name)
              }
              .buttonStyle(.plain)
            }
          }
          .animation(.spring(duration: 0.2), value: results)
        }
        .onAppear {
          results = TagGroup.searchSymbol(for: symbolQuery, exclude: selectedSymbol)
        }
      case let .invalid(description):
        Text(description)
          .font(.subheadline)
          .foregroundStyle(.secondary)
      }
    }
    .onAppear {
      results = TagGroup.searchSymbol(for: symbolQuery, exclude: selectedSymbol)
    }
    .onChange(of: symbolQuery) {
      results = TagGroup.searchSymbol(for: symbolQuery, exclude: selectedSymbol)
    }
  }

  // MARK: search results validation

  enum ValidationStatus: Equatable {
    case valid
    case invalid(description: LocalizedStringKey)
  }

  var validationStatus: ValidationStatus {
    if results.isEmpty {
      if symbolQuery == selectedSymbol,
        !symbolQuery.isEmpty,
        results.count == 0
      {
        .invalid(
          description:
            "\(symbolQuery) add-tag-groups.edit.tags.field.warning.search-results.already-selected")
      } else {
        .invalid(
          description: "add-tag-groups.edit.tags.field.warning.search-results.no-symbol-found")
      }
    } else {
      .valid
    }
  }
}

extension TagGroup {
  // MARK: title validation

  enum TitleValidationStatus: Equatable {
    case valid
    case invalid(description: LocalizedStringKey)
  }

  var titleValidationStatus: TitleValidationStatus {
    title.isEmpty
      ? .invalid(description: "add-tag-groups.edit.title.field.warning.empty-title")
      : .valid
  }

  // MARK: symbolName validation

  enum SymbolNameValidationStatus: Equatable {
    case valid
    case invalid(description: LocalizedStringKey)
  }

  var symbolNameValidationStatus: SymbolNameValidationStatus {
    if symbolName.isEmpty {
      return .invalid(description: "add-tag-groups.edit.title.field.warning.no-symbol-selected")
    } else if !Self.allSymbols.contains(symbolName) {
      return .invalid(
        description: "\(symbolName) add-tag-groups.edit.title.field.warning.invalid-sfsymbol-name")
    }

    return .valid
  }

  // MARK: tags validation

  enum TagsValidationStatus: Equatable {
    case valid
    case invalid(description: LocalizedStringKey)
  }

  var tagsValidationStatus: TagsValidationStatus {
    if tags.count < 2 {
      return .invalid(description: "add-tag-groups.edit.tags.field.warning.number-of-tags")
    }
    return .valid
  }

  // MARK: TagGroup validation

  var isValid: Bool {
    titleValidationStatus == .valid
      && symbolNameValidationStatus == .valid
      && tagsValidationStatus == .valid
  }

  // MARK: format

  func format() {
    title = title.trimmingCharacters(in: .whitespacesAndNewlines)
    tags = tags.map { $0.lowercased() }
  }

  // MARK: static members

  static func emptyGroup() -> TagGroup {
    TagGroup(title: "", symbolName: "", tags: [])
  }

  static func searchSymbol(for query: String, exclude excludedSymbol: String) -> [String] {
    guard !query.isEmpty else { return [] }

    return allSymbols.filter {
      $0.contains(query) && $0 != excludedSymbol
    }
  }

  static let allSymbols: [String] = SFSymbol.allSymbols.map { symbol in
    symbol.rawValue
  }
}

extension Text {
  func warningLabel() -> Text {
    font(.caption)
      .foregroundStyle(.red)
  }
}
