## nimpak/database.nim
## Simple package database for MVP implementation
##
## This module provides a lightweight package database using JSON files
## for storing package metadata, installation records, and dependencies.

import std/[os, json, tables, times, strformat, strutils, sequtils, algorithm]
import types_fixed

type
  PackageDatabase* = ref object
    dbPath*: string
    packages*: Table[string, PackageInfo]
    installed*: Table[string, InstalledPackage]

  PackageInfo* = object
    name*: string
    version*: string
    description*: string
    dependencies*: seq[string]
    size*: int64
    tags*: seq[string]
    stream*: string

  InstalledPackage* = object
    name*: string
    version*: string
    installDate*: DateTime
    size*: int64
    installPath*: string
    stream*: string

const
  DEFAULT_DB_PATH = "~/.nip"
  PACKAGES_DB = "packages.json"
  INSTALLED_DB = "installed.json"

# Forward declarations
proc savePackages*(db: PackageDatabase)
proc saveInstalled*(db: PackageDatabase)

proc newPackageDatabase*(dbPath: string = DEFAULT_DB_PATH): PackageDatabase =
  ## Create a new package database instance
  result = PackageDatabase(
    dbPath: dbPath,
    packages: initTable[string, PackageInfo](),
    installed: initTable[string, InstalledPackage]()
  )

  # Ensure database directory exists
  let expandedPath = if dbPath.startsWith("~"): expandTilde(dbPath) else: dbPath
  createDir(expandedPath)
  result.dbPath = expandedPath

proc savePackages*(db: PackageDatabase) =
  ## Save packages database to disk
  var data = newJObject()
  for name, pkg in db.packages.pairs:
    data[name] = %*{
      "name": pkg.name,
      "version": pkg.version,
      "description": pkg.description,
      "dependencies": pkg.dependencies,
      "size": pkg.size,
      "tags": pkg.tags,
      "stream": pkg.stream
    }

  let packagesFile = db.dbPath / PACKAGES_DB
  writeFile(packagesFile, data.pretty())

proc saveInstalled*(db: PackageDatabase) =
  ## Save installed packages database to disk
  var data = newJObject()
  for name, pkg in db.installed.pairs:
    data[name] = %*{
      "name": pkg.name,
      "version": pkg.version,
      "installDate": pkg.installDate.format("yyyy-MM-dd'T'HH:mm:ss"),
      "size": pkg.size,
      "installPath": pkg.installPath,
      "stream": pkg.stream
    }

  let installedFile = db.dbPath / INSTALLED_DB
  writeFile(installedFile, data.pretty())

proc initSamplePackages*(db: PackageDatabase) =
  ## Initialize database with sample packages for MVP
  db.packages["htop"] = PackageInfo(
    name: "htop",
    version: "3.2.2",
    description: "Interactive process viewer",
    dependencies: @["ncurses"],
    size: 2_000_000,
    tags: @["monitoring", "cli"],
    stream: "stable"
  )

  db.packages["neofetch"] = PackageInfo(
    name: "neofetch",
    version: "7.1.0",
    description: "System information tool",
    dependencies: @[],
    size: 500_000,
    tags: @["system", "info", "cli"],
    stream: "stable"
  )

  db.packages["vim"] = PackageInfo(
    name: "vim",
    version: "9.0.2",
    description: "Vi IMproved text editor",
    dependencies: @["ncurses"],
    size: 15_000_000,
    tags: @["editor", "cli"],
    stream: "stable"
  )

  db.packages["git"] = PackageInfo(
    name: "git",
    version: "2.41.0",
    description: "Distributed version control system",
    dependencies: @["curl", "openssl"],
    size: 43_800_000,
    tags: @["vcs", "development"],
    stream: "stable"
  )

  db.packages["firefox"] = PackageInfo(
    name: "firefox",
    version: "118.0",
    description: "Mozilla Firefox web browser",
    dependencies: @["gtk3", "dbus"],
    size: 250_000_000,
    tags: @["browser", "gui"],
    stream: "stable"
  )

  db.packages["ncurses"] = PackageInfo(
    name: "ncurses",
    version: "6.4",
    description: "Terminal interface library",
    dependencies: @[],
    size: 1_500_000,
    tags: @["library", "terminal"],
    stream: "stable"
  )

  db.packages["curl"] = PackageInfo(
    name: "curl",
    version: "8.4.0",
    description: "Command line tool for transferring data",
    dependencies: @["openssl"],
    size: 3_200_000,
    tags: @["network", "cli"],
    stream: "stable"
  )

  db.packages["openssl"] = PackageInfo(
    name: "openssl",
    version: "3.1.4",
    description: "Cryptography and SSL/TLS toolkit",
    dependencies: @[],
    size: 8_500_000,
    tags: @["crypto", "library"],
    stream: "stable"
  )

  # Save the sample data
  db.savePackages()

proc loadPackages*(db: PackageDatabase) =
  ## Load available packages from database
  let packagesFile = db.dbPath / PACKAGES_DB
  if fileExists(packagesFile):
    try:
      let data = parseFile(packagesFile)
      for name, info in data.pairs:
        db.packages[name] = PackageInfo(
          name: info["name"].getStr(),
          version: info["version"].getStr(),
          description: info["description"].getStr(),
          dependencies: info["dependencies"].getElems().mapIt(it.getStr()),
          size: info["size"].getInt(),
          tags: info["tags"].getElems().mapIt(it.getStr()),
          stream: info.getOrDefault("stream").getStr("stable")
        )
    except:
      # Initialize with sample packages if file is corrupted
      db.initSamplePackages()
  else:
    # Create initial sample database
    db.initSamplePackages()

proc loadInstalled*(db: PackageDatabase) =
  ## Load installed packages from database
  let installedFile = db.dbPath / INSTALLED_DB
  if fileExists(installedFile):
    try:
      let data = parseFile(installedFile)
      for name, info in data.pairs:
        db.installed[name] = InstalledPackage(
          name: info["name"].getStr(),
          version: info["version"].getStr(),
          installDate: info["installDate"].getStr().parse("yyyy-MM-dd'T'HH:mm:ss"),
          size: info["size"].getInt(),
          installPath: info["installPath"].getStr(),
          stream: info.getOrDefault("stream").getStr("stable")
        )
    except:
      discard # Empty installed database

proc searchPackages*(db: PackageDatabase, query: string): seq[PackageInfo] =
  ## Search for packages matching query
  result = @[]
  let lowerQuery = query.toLower()

  for pkg in db.packages.values:
    if lowerQuery in pkg.name.toLower() or
       lowerQuery in pkg.description.toLower() or
       pkg.tags.anyIt(lowerQuery in it.toLower()):
      result.add(pkg)

proc getPackage*(db: PackageDatabase, name: string): PackageInfo =
  ## Get package info by name
  if name in db.packages:
    return db.packages[name]
  else:
    raise newException(KeyError, "Package not found: " & name)

proc isInstalled*(db: PackageDatabase, name: string): bool =
  ## Check if package is installed
  name in db.installed

proc installPackage*(db: PackageDatabase, name: string): bool =
  ## Install a package (MVP implementation)
  if name notin db.packages:
    return false

  if db.isInstalled(name):
    return true # Already installed

  let pkg = db.packages[name]
  let installPath = expandTilde("~/.nip/programs") / name / "current"

  # Create installation directory
  createDir(installPath)
  createDir(installPath / "bin")
  createDir(installPath / "lib")

  # Simulate package installation by creating placeholder files
  writeFile(installPath / "bin" / name, fmt"#!/bin/sh\necho 'Running {name} v{pkg.version}'\n")
  writeFile(installPath / "lib" / fmt"lib{name}.so", fmt"# {name} library v{pkg.version}\n")

  # Make binary executable
  setFilePermissions(installPath / "bin" / name, {fpUserExec, fpUserRead, fpUserWrite})

  # Create system links
  let binLink = expandTilde("~/.nip/bin") / name
  let libLink = expandTilde("~/.nip/lib") / fmt"lib{name}.so"

  createDir(expandTilde("~/.nip/bin"))
  createDir(expandTilde("~/.nip/lib"))

  try:
    createSymlink(installPath / "bin" / name, binLink)
    createSymlink(installPath / "lib" / fmt"lib{name}.so", libLink)
  except:
    discard # Links might already exist

  # Record installation
  db.installed[name] = InstalledPackage(
    name: name,
    version: pkg.version,
    installDate: now(),
    size: pkg.size,
    installPath: installPath,
    stream: pkg.stream
  )

  db.saveInstalled()
  return true

proc removePackage*(db: PackageDatabase, name: string): bool =
  ## Remove an installed package
  if not db.isInstalled(name):
    return false

  let installed = db.installed[name]

  # Remove system links
  let binLink = expandTilde("~/.nip/bin") / name
  let libLink = expandTilde("~/.nip/lib") / fmt"lib{name}.so"

  try:
    removeFile(binLink)
    removeFile(libLink)
  except:
    discard

  # Remove installation directory
  try:
    removeDir(installed.installPath)
  except:
    discard

  # Remove from installed database
  db.installed.del(name)
  db.saveInstalled()

  return true

proc listInstalled*(db: PackageDatabase): seq[InstalledPackage] =
  ## List all installed packages
  result = @[]
  for pkg in db.installed.values:
    result.add(pkg)

  # Sort by installation date (newest first)
  result.sort(proc(a, b: InstalledPackage): int =
    cmp(b.installDate, a.installDate))

proc getTotalInstalledSize*(db: PackageDatabase): int64 =
  ## Get total size of installed packages
  result = 0
  for pkg in db.installed.values:
    result += pkg.size

proc initDatabase*(db: PackageDatabase) =
  ## Initialize database by loading existing data
  db.loadPackages()
  db.loadInstalled()