//
// Copyright (c) Double Symmetry GmbH
// Commercial use requires a license. See https://rntp.dev/pricing
//

import Foundation
import CryptoKit

final class AudioCache: @unchecked Sendable {

  struct ContentInfo: Codable {
    let contentType: String
    let contentLength: Int64
    let isByteRangeAccessSupported: Bool
  }

  private struct ItemMeta: Codable {
    var url: String
    var contentInfo: ContentInfo?
    var lastAccessDate: Date
  }

  let maxSize: Int64
  private let root: URL
  private let queue = DispatchQueue(label: "trackplayer.cache", qos: .utility)

  init(maxSizeBytes: Int64, directory: URL? = nil) {
    self.maxSize = maxSizeBytes
    if let directory = directory {
      self.root = directory
    } else {
      let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
      self.root = caches.appendingPathComponent("TrackPlayerCache", isDirectory: true)
    }
    try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
  }

  // MARK: - Key

  func cacheKey(for url: URL) -> String {
    let hash = SHA256.hash(data: Data(url.absoluteString.utf8))
    return hash.compactMap { String(format: "%02x", $0) }.joined()
  }

  // MARK: - Content Info

  func contentInfo(for key: String) -> ContentInfo? {
    return queue.sync { meta(for: key)?.contentInfo }
  }

  func storeContentInfo(_ info: ContentInfo, for key: String, url: URL) {
    queue.sync {
      var m = meta(for: key) ?? ItemMeta(url: url.absoluteString, lastAccessDate: Date())
      m.contentInfo = info
      writeMeta(m, for: key)
    }
  }

  // MARK: - Data

  func cachedBytes(for key: String) -> Int64 {
    return queue.sync { dataFileSize(for: key) }
  }

  func isFullyCached(key: String) -> Bool {
    return queue.sync {
      guard let info = meta(for: key)?.contentInfo, info.contentLength > 0 else { return false }
      return dataFileSize(for: key) >= info.contentLength
    }
  }

  func readData(for key: String, offset: Int64, length: Int) -> Data? {
    return queue.sync {
      let path = dataFileURL(for: key).path
      guard let handle = FileHandle(forReadingAtPath: path) else { return nil }
      defer { try? handle.close() }
      handle.seek(toFileOffset: UInt64(offset))
      let data = handle.readData(ofLength: length)
      return data.isEmpty ? nil : data
    }
  }

  func appendData(_ data: Data, for key: String) {
    queue.sync {
      let url = dataFileURL(for: key)
      if FileManager.default.fileExists(atPath: url.path) {
        guard let handle = try? FileHandle(forWritingTo: url) else { return }
        defer { try? handle.close() }
        handle.seekToEndOfFile()
        handle.write(data)
      } else {
        try? data.write(to: url)
      }
    }
  }

  // MARK: - Access Tracking

  func touchAccessDate(for key: String) {
    queue.sync {
      guard var m = meta(for: key) else { return }
      m.lastAccessDate = Date()
      writeMeta(m, for: key)
    }
  }

  // MARK: - Eviction

  func evictIfNeeded() {
    queue.sync { _evictIfNeeded() }
  }

  func removeAll() {
    queue.sync {
      try? FileManager.default.removeItem(at: root)
      try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
    }
  }

  // MARK: - Debug

  /// Dumps the full cache state to the console for debugging.
  func dumpState(currentKey: String? = nil) {
    queue.sync {
      let fm = FileManager.default
      guard let contents = try? fm.contentsOfDirectory(at: root, includingPropertiesForKeys: nil) else {
        print("[AudioCache] empty or unreadable")
        return
      }

      let metaFiles = contents.filter { $0.pathExtension == "meta" }
      let totalSize = metaFiles.reduce(Int64(0)) { total, metaURL in
        let key = metaURL.deletingPathExtension().lastPathComponent
        return total + dataFileSize(for: key)
      }

      print("[AudioCache] \(metaFiles.count) items, \(totalSize / 1024 / 1024)MB / \(maxSize / 1024 / 1024)MB max")

      for metaURL in metaFiles {
        let key = metaURL.deletingPathExtension().lastPathComponent
        let m = meta(for: key)
        let size = dataFileSize(for: key)
        let cl = m?.contentInfo?.contentLength ?? -1
        let ct = m?.contentInfo?.contentType ?? "?"
        let url = m?.url ?? "?"
        let pct = cl > 0 ? Int(Double(size) / Double(cl) * 100) : -1
        let isCurrent = (key == currentKey) ? " ← CURRENT" : ""
        let urlShort = url.count > 60 ? "…" + url.suffix(55) : url
        print("  [\(key.prefix(8))…] \(size / 1024)KB / \(cl > 0 ? "\(cl / 1024)KB" : "??") (\(pct)%) \(ct) \(urlShort)\(isCurrent)")
      }
    }
  }

  // MARK: - Internal File Helpers

  private func dataFileURL(for key: String) -> URL {
    root.appendingPathComponent(key + ".data")
  }

  private func metaFileURL(for key: String) -> URL {
    root.appendingPathComponent(key + ".meta")
  }

  private func dataFileSize(for key: String) -> Int64 {
    let path = dataFileURL(for: key).path
    guard let attrs = try? FileManager.default.attributesOfItem(atPath: path),
          let size = attrs[.size] as? Int64 else { return 0 }
    return size
  }

  private func meta(for key: String) -> ItemMeta? {
    let url = metaFileURL(for: key)
    guard let data = try? Data(contentsOf: url) else { return nil }
    return try? JSONDecoder().decode(ItemMeta.self, from: data)
  }

  private func writeMeta(_ meta: ItemMeta, for key: String) {
    let url = metaFileURL(for: key)
    guard let data = try? JSONEncoder().encode(meta) else { return }
    try? data.write(to: url, options: .atomic)
  }

  private func removeItem(for key: String) {
    try? FileManager.default.removeItem(at: dataFileURL(for: key))
    try? FileManager.default.removeItem(at: metaFileURL(for: key))
  }

  private func _evictIfNeeded() {
    let fm = FileManager.default
    guard let contents = try? fm.contentsOfDirectory(at: root, includingPropertiesForKeys: nil) else { return }

    let metaFiles = contents.filter { $0.pathExtension == "meta" }
    var items: [(key: String, size: Int64, date: Date)] = []

    for metaURL in metaFiles {
      let key = metaURL.deletingPathExtension().lastPathComponent
      let size = dataFileSize(for: key)
      let date = meta(for: key)?.lastAccessDate ?? .distantPast
      items.append((key, size, date))
    }

    var totalSize = items.reduce(Int64(0)) { $0 + $1.size }
    guard totalSize > maxSize else { return }

    items.sort { $0.date < $1.date }

    for item in items {
      guard totalSize > maxSize else { break }
      removeItem(for: item.key)
      totalSize -= item.size
    }
  }
}
