package db

import (
	"database/sql"
	"fmt"
	"time"

	_ "modernc.org/sqlite"
)

// Store wraps the SQLite database.
type Store struct {
	db *sql.DB
}

// Open opens (or creates) the SQLite database at path and runs migrations.
func Open(path string) (*Store, error) {
	conn, err := sql.Open("sqlite", path)
	if err != nil {
		return nil, fmt.Errorf("open sqlite at %q: %w", path, err)
	}

	// SQLite works best with a single writer connection.
	conn.SetMaxOpenConns(1)

	// Recommended pragmas for reliability and performance.
	for _, pragma := range []string{
		"PRAGMA journal_mode=WAL",
		"PRAGMA busy_timeout=5000",
		"PRAGMA foreign_keys=ON",
	} {
		if _, err := conn.Exec(pragma); err != nil {
			conn.Close()
			return nil, fmt.Errorf("pragma %q: %w", pragma, err)
		}
	}

	s := &Store{db: conn}
	if err := s.migrate(); err != nil {
		conn.Close()
		return nil, fmt.Errorf("migrate: %w", err)
	}

	return s, nil
}

// Close closes the underlying database connection.
func (s *Store) Close() error {
	return s.db.Close()
}

func (s *Store) migrate() error {
	_, err := s.db.Exec(`
		CREATE TABLE IF NOT EXISTS host_ports (
			ip         TEXT     NOT NULL,
			port       INTEGER  NOT NULL,
			scanned_at DATETIME NOT NULL,
			PRIMARY KEY (ip, port)
		);

		CREATE TABLE IF NOT EXISTS snmp_targets (
			id         INTEGER  PRIMARY KEY AUTOINCREMENT,
			name       TEXT     NOT NULL,
			ip         TEXT     NOT NULL,
			community  TEXT     NOT NULL DEFAULT 'public',
			version    TEXT     NOT NULL DEFAULT 'v2c',
			type       TEXT     NOT NULL DEFAULT 'generic',
			enabled    INTEGER  NOT NULL DEFAULT 1,
			created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
		);

		CREATE TABLE IF NOT EXISTS snmp_results (
			id         INTEGER  PRIMARY KEY AUTOINCREMENT,
			target_id  INTEGER  NOT NULL,
			oid_name   TEXT     NOT NULL,
			value      TEXT     NOT NULL DEFAULT '',
			scanned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
			UNIQUE(target_id, oid_name)
		);

		CREATE TABLE IF NOT EXISTS module_states (
			name    TEXT PRIMARY KEY,
			enabled INTEGER NOT NULL DEFAULT 1
		);

		CREATE TABLE IF NOT EXISTS monitor_checks (
			id         INTEGER  PRIMARY KEY AUTOINCREMENT,
			name       TEXT     NOT NULL,
			type       TEXT     NOT NULL,
			target     TEXT     NOT NULL,
			enabled    INTEGER  NOT NULL DEFAULT 1,
			created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
		);

		CREATE TABLE IF NOT EXISTS monitor_results (
			id         INTEGER  PRIMARY KEY AUTOINCREMENT,
			check_id   INTEGER  NOT NULL,
			status     TEXT     NOT NULL,
			latency_ms INTEGER  NOT NULL DEFAULT 0,
			error      TEXT     NOT NULL DEFAULT '',
			checked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
		);

		CREATE INDEX IF NOT EXISTS idx_monitor_results_check
			ON monitor_results(check_id, checked_at DESC);

		CREATE TABLE IF NOT EXISTS ad_computers (
			id           INTEGER  PRIMARY KEY AUTOINCREMENT,
			cn           TEXT     NOT NULL UNIQUE,
			dn           TEXT     NOT NULL DEFAULT '',
			os           TEXT     NOT NULL DEFAULT '',
			os_version   TEXT     NOT NULL DEFAULT '',
			owner        TEXT     NOT NULL DEFAULT '',
			department   TEXT     NOT NULL DEFAULT '',
			last_logon   DATETIME,
			ad_created   DATETIME,
			updated_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
		);

		CREATE TABLE IF NOT EXISTS hosts (
			id         INTEGER  PRIMARY KEY AUTOINCREMENT,
			ip         TEXT     NOT NULL UNIQUE,
			mac        TEXT     NOT NULL,
			vendor     TEXT     NOT NULL DEFAULT '',
			hostname   TEXT     NOT NULL DEFAULT '',
			site       TEXT     NOT NULL,
			first_seen DATETIME NOT NULL,
			last_seen  DATETIME NOT NULL,
			status     TEXT     NOT NULL DEFAULT 'online'
		);

		CREATE TABLE IF NOT EXISTS scan_runs (
			id         INTEGER  PRIMARY KEY AUTOINCREMENT,
			module     TEXT     NOT NULL,
			started_at DATETIME NOT NULL,
			ended_at   DATETIME,
			status     TEXT     NOT NULL DEFAULT 'running',
			error      TEXT     NOT NULL DEFAULT ''
		);
	`)
	return err
}

// Host represents a discovered network device.
type Host struct {
	ID        int64
	IP        string
	MAC       string
	Vendor    string
	Hostname  string
	Site      string
	FirstSeen time.Time
	LastSeen  time.Time
	Status    string
}

// UpsertHost inserts a new host or updates MAC, vendor and last_seen if the
// IP already exists. first_seen is never overwritten on update.
func (s *Store) UpsertHost(h Host) error {
	_, err := s.db.Exec(`
		INSERT INTO hosts (ip, mac, vendor, hostname, site, first_seen, last_seen, status)
		VALUES (?, ?, ?, ?, ?, ?, ?, 'online')
		ON CONFLICT(ip) DO UPDATE SET
			mac       = excluded.mac,
			vendor    = CASE WHEN excluded.vendor != '' THEN excluded.vendor ELSE vendor END,
			hostname  = CASE WHEN excluded.hostname != '' THEN excluded.hostname ELSE hostname END,
			last_seen = excluded.last_seen,
			status    = 'online'
	`, h.IP, h.MAC, h.Vendor, h.Hostname, h.Site, h.FirstSeen.UTC(), h.LastSeen.UTC())
	if err != nil {
		return fmt.Errorf("upsert host %s: %w", h.IP, err)
	}
	return nil
}

// ListHosts returns all hosts ordered by last_seen desc.
// Pass an empty site to return all sites.
func (s *Store) ListHosts(site string) ([]Host, error) {
	query := `
		SELECT id, ip, mac, vendor, hostname, site, first_seen, last_seen, status
		FROM hosts`
	var args []any
	if site != "" {
		query += ` WHERE site = ?`
		args = append(args, site)
	}
	query += ` ORDER BY last_seen DESC`

	rows, err := s.db.Query(query, args...)
	if err != nil {
		return nil, fmt.Errorf("list hosts: %w", err)
	}
	defer rows.Close()

	var hosts []Host
	for rows.Next() {
		var h Host
		if err := rows.Scan(
			&h.ID, &h.IP, &h.MAC, &h.Vendor, &h.Hostname,
			&h.Site, &h.FirstSeen, &h.LastSeen, &h.Status,
		); err != nil {
			return nil, fmt.Errorf("scan host row: %w", err)
		}
		hosts = append(hosts, h)
	}
	return hosts, rows.Err()
}

// CountHosts returns the total number of hosts in the database.
func (s *Store) CountHosts() (int, error) {
	var n int
	if err := s.db.QueryRow(`SELECT COUNT(*) FROM hosts`).Scan(&n); err != nil {
		return 0, fmt.Errorf("count hosts: %w", err)
	}
	return n, nil
}

// BeginScan records the start of a module scan run and returns the run ID.
func (s *Store) BeginScan(module string) (int64, error) {
	res, err := s.db.Exec(`
		INSERT INTO scan_runs (module, started_at) VALUES (?, ?)
	`, module, time.Now().UTC())
	if err != nil {
		return 0, fmt.Errorf("begin scan: %w", err)
	}
	return res.LastInsertId()
}

// EndScan marks a scan run as completed (status ok or error).
func (s *Store) EndScan(id int64, scanErr error) error {
	status, errMsg := "ok", ""
	if scanErr != nil {
		status, errMsg = "error", scanErr.Error()
	}
	_, err := s.db.Exec(`
		UPDATE scan_runs SET ended_at = ?, status = ?, error = ? WHERE id = ?
	`, time.Now().UTC(), status, errMsg, id)
	if err != nil {
		return fmt.Errorf("end scan %d: %w", id, err)
	}
	return nil
}

// ADComputer represents a computer object from Active Directory.
type ADComputer struct {
	ID         int64
	CN         string
	DN         string
	OS         string
	OSVersion  string
	Owner      string
	Department string
	LastLogon  time.Time
	ADCreated  time.Time
	UpdatedAt  time.Time
}

// UpsertADComputer inserts or updates an AD computer record by CN.
func (s *Store) UpsertADComputer(c ADComputer) error {
	_, err := s.db.Exec(`
		INSERT INTO ad_computers (cn, dn, os, os_version, owner, department, last_logon, ad_created, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
		ON CONFLICT(cn) DO UPDATE SET
			dn         = excluded.dn,
			os         = excluded.os,
			os_version = excluded.os_version,
			owner      = excluded.owner,
			department = excluded.department,
			last_logon = excluded.last_logon,
			updated_at = excluded.updated_at
	`, c.CN, c.DN, c.OS, c.OSVersion, c.Owner, c.Department,
		nullTime(c.LastLogon), nullTime(c.ADCreated), time.Now().UTC())
	if err != nil {
		return fmt.Errorf("upsert ad computer %s: %w", c.CN, err)
	}
	return nil
}

// ListADComputers returns all known AD computers.
func (s *Store) ListADComputers() ([]ADComputer, error) {
	rows, err := s.db.Query(`
		SELECT id, cn, dn, os, os_version, owner, department, last_logon, ad_created, updated_at
		FROM ad_computers ORDER BY cn
	`)
	if err != nil {
		return nil, fmt.Errorf("list ad computers: %w", err)
	}
	defer rows.Close()

	var out []ADComputer
	for rows.Next() {
		var c ADComputer
		var lastLogon, adCreated *time.Time
		if err := rows.Scan(&c.ID, &c.CN, &c.DN, &c.OS, &c.OSVersion,
			&c.Owner, &c.Department, &lastLogon, &adCreated, &c.UpdatedAt); err != nil {
			return nil, err
		}
		if lastLogon != nil {
			c.LastLogon = *lastLogon
		}
		if adCreated != nil {
			c.ADCreated = *adCreated
		}
		out = append(out, c)
	}
	return out, rows.Err()
}

// UpdateHostnameFromAD sets the hostname of a host matching the given IP.
func (s *Store) UpdateHostnameFromAD(hostname string) error {
	_, err := s.db.Exec(`
		UPDATE hosts SET hostname = ? WHERE hostname = '' AND ip IN (
			SELECT ip FROM hosts WHERE LOWER(hostname) = LOWER(?)
		)
	`, hostname, hostname)
	return err
}

func nullTime(t time.Time) any {
	if t.IsZero() {
		return nil
	}
	return t.UTC()
}

// LastScanTime returns when the given module last ran successfully.
func (s *Store) LastScanTime(module string) (time.Time, error) {
	var t time.Time
	err := s.db.QueryRow(`
		SELECT ended_at FROM scan_runs
		WHERE module = ? AND status = 'ok'
		ORDER BY ended_at DESC LIMIT 1
	`, module).Scan(&t)
	if err == sql.ErrNoRows {
		return time.Time{}, nil
	}
	return t, err
}

/* ── Site Monitoring ──────────────────────────────────────────────── */

// MonitorCheck is a configured availability check (ping/http/tcp).
type MonitorCheck struct {
	ID        int64
	Name      string
	Type      string // ping | http | tcp
	Target    string
	Enabled   bool
	CreatedAt time.Time
}

// MonitorResult is a single check execution result.
type MonitorResult struct {
	ID        int64
	CheckID   int64
	Status    string // online | offline
	LatencyMS int
	Error     string
	CheckedAt time.Time
}

// ListMonitorChecks returns all configured checks ordered by creation time.
func (s *Store) ListMonitorChecks() ([]MonitorCheck, error) {
	rows, err := s.db.Query(`
		SELECT id, name, type, target, enabled, created_at
		FROM monitor_checks ORDER BY id
	`)
	if err != nil {
		return nil, fmt.Errorf("list monitor checks: %w", err)
	}
	defer rows.Close()
	var out []MonitorCheck
	for rows.Next() {
		var c MonitorCheck
		var ena int
		if err := rows.Scan(&c.ID, &c.Name, &c.Type, &c.Target, &ena, &c.CreatedAt); err != nil {
			return nil, err
		}
		c.Enabled = ena == 1
		out = append(out, c)
	}
	return out, rows.Err()
}

// AddMonitorCheck inserts a new check definition.
func (s *Store) AddMonitorCheck(name, typ, target string) (int64, error) {
	res, err := s.db.Exec(`
		INSERT INTO monitor_checks (name, type, target) VALUES (?, ?, ?)
	`, name, typ, target)
	if err != nil {
		return 0, fmt.Errorf("add monitor check: %w", err)
	}
	return res.LastInsertId()
}

// DeleteMonitorCheck removes a check and all its results.
func (s *Store) DeleteMonitorCheck(id int64) error {
	if _, err := s.db.Exec(`DELETE FROM monitor_results WHERE check_id = ?`, id); err != nil {
		return fmt.Errorf("delete monitor results: %w", err)
	}
	if _, err := s.db.Exec(`DELETE FROM monitor_checks WHERE id = ?`, id); err != nil {
		return fmt.Errorf("delete monitor check: %w", err)
	}
	return nil
}

// InsertMonitorResult stores one check execution result.
func (s *Store) InsertMonitorResult(r MonitorResult) error {
	_, err := s.db.Exec(`
		INSERT INTO monitor_results (check_id, status, latency_ms, error, checked_at)
		VALUES (?, ?, ?, ?, ?)
	`, r.CheckID, r.Status, r.LatencyMS, r.Error, r.CheckedAt.UTC())
	if err != nil {
		return fmt.Errorf("insert monitor result: %w", err)
	}
	return nil
}

// LatestMonitorResults returns the most recent result for each check_id.
func (s *Store) LatestMonitorResults() (map[int64]MonitorResult, error) {
	rows, err := s.db.Query(`
		SELECT r.id, r.check_id, r.status, r.latency_ms, r.error, r.checked_at
		FROM monitor_results r
		INNER JOIN (
			SELECT check_id, MAX(id) as max_id FROM monitor_results GROUP BY check_id
		) latest ON r.id = latest.max_id
	`)
	if err != nil {
		return nil, fmt.Errorf("latest monitor results: %w", err)
	}
	defer rows.Close()
	out := make(map[int64]MonitorResult)
	for rows.Next() {
		var r MonitorResult
		if err := rows.Scan(&r.ID, &r.CheckID, &r.Status, &r.LatencyMS, &r.Error, &r.CheckedAt); err != nil {
			return nil, err
		}
		out[r.CheckID] = r
	}
	return out, rows.Err()
}

// MonitorResultHistory returns the last limit results for a given check.
func (s *Store) MonitorResultHistory(checkID int64, limit int) ([]MonitorResult, error) {
	rows, err := s.db.Query(`
		SELECT id, check_id, status, latency_ms, error, checked_at
		FROM monitor_results WHERE check_id = ?
		ORDER BY checked_at DESC LIMIT ?
	`, checkID, limit)
	if err != nil {
		return nil, fmt.Errorf("monitor history: %w", err)
	}
	defer rows.Close()
	var out []MonitorResult
	for rows.Next() {
		var r MonitorResult
		if err := rows.Scan(&r.ID, &r.CheckID, &r.Status, &r.LatencyMS, &r.Error, &r.CheckedAt); err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	// Reverse so oldest-first for sparkline rendering.
	for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
		out[i], out[j] = out[j], out[i]
	}
	return out, rows.Err()
}

/* ── Module States ────────────────────────────────────────────────── */

// SetModuleEnabled stores the enabled state for a module.
func (s *Store) SetModuleEnabled(name string, enabled bool) error {
	v := 0
	if enabled {
		v = 1
	}
	_, err := s.db.Exec(`
		INSERT INTO module_states (name, enabled) VALUES (?,?)
		ON CONFLICT(name) DO UPDATE SET enabled=excluded.enabled
	`, name, v)
	return err
}

// GetModuleEnabled returns whether a module is enabled (default true if not set).
func (s *Store) GetModuleEnabled(name string) bool {
	var v int
	err := s.db.QueryRow(`SELECT enabled FROM module_states WHERE name=?`, name).Scan(&v)
	if err != nil {
		return true // default: enabled
	}
	return v == 1
}

/* ── Port Scan ────────────────────────────────────────────────────── */

// UpsertHostPorts replaces all open ports for an IP.
func (s *Store) UpsertHostPorts(ip string, ports []int) error {
	tx, err := s.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()
	if _, err := tx.Exec(`DELETE FROM host_ports WHERE ip = ?`, ip); err != nil {
		return err
	}
	now := time.Now().UTC()
	for _, p := range ports {
		if _, err := tx.Exec(`INSERT INTO host_ports (ip, port, scanned_at) VALUES (?,?,?)`, ip, p, now); err != nil {
			return err
		}
	}
	return tx.Commit()
}

// GetHostPorts returns all open ports for an IP.
func (s *Store) GetHostPorts(ip string) ([]int, error) {
	rows, err := s.db.Query(`SELECT port FROM host_ports WHERE ip = ? ORDER BY port`, ip)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var ports []int
	for rows.Next() {
		var p int
		rows.Scan(&p)
		ports = append(ports, p)
	}
	return ports, rows.Err()
}

// ListAllHostPorts returns a map of IP → open ports.
func (s *Store) ListAllHostPorts() (map[string][]int, error) {
	rows, err := s.db.Query(`SELECT ip, port FROM host_ports ORDER BY ip, port`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make(map[string][]int)
	for rows.Next() {
		var ip string
		var port int
		rows.Scan(&ip, &port)
		out[ip] = append(out[ip], port)
	}
	return out, rows.Err()
}

// MarkStaleHostsOffline marks hosts as offline if last_seen is older than threshold.
func (s *Store) MarkStaleHostsOffline(olderThan time.Duration) (int, error) {
	threshold := time.Now().UTC().Add(-olderThan)
	res, err := s.db.Exec(`UPDATE hosts SET status='offline' WHERE status='online' AND last_seen < ?`, threshold)
	if err != nil {
		return 0, fmt.Errorf("mark stale offline: %w", err)
	}
	n, _ := res.RowsAffected()
	return int(n), nil
}

/* ── SNMP ─────────────────────────────────────────────────────────── */

// SNMPTarget is a configured SNMP polling target.
type SNMPTarget struct {
	ID        int64
	Name      string
	IP        string
	Community string
	Version   string
	Type      string // generic | printer | switch
	Enabled   bool
	CreatedAt time.Time
}

// SNMPResult is the latest value for one OID on one target.
type SNMPResult struct {
	ID        int64
	TargetID  int64
	OIDName   string
	Value     string
	ScannedAt time.Time
}

// ListSNMPTargets returns all SNMP targets.
func (s *Store) ListSNMPTargets() ([]SNMPTarget, error) {
	rows, err := s.db.Query(`SELECT id,name,ip,community,version,type,enabled,created_at FROM snmp_targets ORDER BY id`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []SNMPTarget
	for rows.Next() {
		var t SNMPTarget
		var ena int
		rows.Scan(&t.ID, &t.Name, &t.IP, &t.Community, &t.Version, &t.Type, &ena, &t.CreatedAt)
		t.Enabled = ena == 1
		out = append(out, t)
	}
	return out, rows.Err()
}

// AddSNMPTarget inserts a new SNMP target.
func (s *Store) AddSNMPTarget(name, ip, community, version, typ string) (int64, error) {
	res, err := s.db.Exec(`INSERT INTO snmp_targets (name,ip,community,version,type) VALUES (?,?,?,?,?)`,
		name, ip, community, version, typ)
	if err != nil {
		return 0, err
	}
	return res.LastInsertId()
}

// DeleteSNMPTarget removes a target and its results.
func (s *Store) DeleteSNMPTarget(id int64) error {
	s.db.Exec(`DELETE FROM snmp_results WHERE target_id = ?`, id)
	_, err := s.db.Exec(`DELETE FROM snmp_targets WHERE id = ?`, id)
	return err
}

// UpsertSNMPResult inserts or updates one OID result.
func (s *Store) UpsertSNMPResult(r SNMPResult) error {
	_, err := s.db.Exec(`
		INSERT INTO snmp_results (target_id, oid_name, value, scanned_at)
		VALUES (?,?,?,?)
		ON CONFLICT(target_id, oid_name) DO UPDATE SET value=excluded.value, scanned_at=excluded.scanned_at
	`, r.TargetID, r.OIDName, r.Value, r.ScannedAt.UTC())
	return err
}

// LatestSNMPResults returns all latest OID values keyed by target_id.
func (s *Store) LatestSNMPResults() (map[int64]map[string]string, error) {
	rows, err := s.db.Query(`SELECT target_id, oid_name, value FROM snmp_results ORDER BY target_id, oid_name`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make(map[int64]map[string]string)
	for rows.Next() {
		var tid int64
		var k, v string
		rows.Scan(&tid, &k, &v)
		if out[tid] == nil {
			out[tid] = make(map[string]string)
		}
		out[tid][k] = v
	}
	return out, rows.Err()
}

// CleanupMonitorResults deletes old results, keeping the last keepPerCheck per check.
func (s *Store) CleanupMonitorResults(keepPerCheck int) error {
	_, err := s.db.Exec(`
		DELETE FROM monitor_results WHERE id NOT IN (
			SELECT id FROM (
				SELECT id, ROW_NUMBER() OVER (
					PARTITION BY check_id ORDER BY checked_at DESC
				) AS rn FROM monitor_results
			) WHERE rn <= ?
		)
	`, keepPerCheck)
	return err
}
