package store

import (
	"crypto/rand"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"net"
	"time"

	_ "modernc.org/sqlite"
)

type Store struct {
	DB *sql.DB
}

type Tenant struct {
	ID     int64
	Name   string
	Status int
	Subnet string
}

type APIKey struct {
	ID       int64
	TenantID int64
	Hash     string
	Scope    string
	Expires  *time.Time
	Status   int
}

type NodeCredential struct {
	NodeID     int64
	NodeName   string
	Secret     string
	VirtualIP  string
	TenantID   int64
}

type EnrollToken struct {
	ID         int64
	TenantID   int64
	Hash       string
	ExpiresAt  int64
	UsedAt     *int64
	MaxAttempt int
	Attempts   int
	Status     int
}

func Open(dbPath string) (*Store, error) {
	db, err := sql.Open("sqlite", dbPath)
	if err != nil {
		return nil, err
	}
	if _, err := db.Exec(`PRAGMA journal_mode=WAL;`); err != nil {
		return nil, err
	}
	if _, err := db.Exec(`PRAGMA foreign_keys=ON;`); err != nil {
		return nil, err
	}
	s := &Store{DB: db}
	if err := s.migrate(); err != nil {
		return nil, err
	}
	if err := s.ensureSubnetPool(); err != nil {
		return nil, err
	}
	return s, nil
}

func (s *Store) migrate() error {
	stmts := []string{
		`CREATE TABLE IF NOT EXISTS tenants (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			name TEXT NOT NULL UNIQUE,
			status INTEGER NOT NULL DEFAULT 1,
			subnet TEXT NOT NULL UNIQUE,
			created_at INTEGER NOT NULL
		);`,
		`CREATE TABLE IF NOT EXISTS users (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			tenant_id INTEGER NOT NULL,
			role TEXT NOT NULL,
			email TEXT,
			password_hash TEXT,
			status INTEGER NOT NULL DEFAULT 1,
			created_at INTEGER NOT NULL,
			FOREIGN KEY(tenant_id) REFERENCES tenants(id)
		);`,
		`CREATE TABLE IF NOT EXISTS api_keys (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			tenant_id INTEGER NOT NULL,
			key_hash TEXT NOT NULL UNIQUE,
			scope TEXT,
			expires_at INTEGER,
			status INTEGER NOT NULL DEFAULT 1,
			created_at INTEGER NOT NULL,
			FOREIGN KEY(tenant_id) REFERENCES tenants(id)
		);`,
		`CREATE TABLE IF NOT EXISTS nodes (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			tenant_id INTEGER NOT NULL,
			node_name TEXT NOT NULL,
			node_pubkey TEXT,
			node_secret_hash TEXT,
			virtual_ip TEXT,
			status INTEGER NOT NULL DEFAULT 1,
			last_seen INTEGER,
			FOREIGN KEY(tenant_id) REFERENCES tenants(id)
		);`,
		`CREATE TABLE IF NOT EXISTS enroll_tokens (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			tenant_id INTEGER NOT NULL,
			token_hash TEXT NOT NULL UNIQUE,
			expires_at INTEGER NOT NULL,
			used_at INTEGER,
			max_attempt INTEGER NOT NULL DEFAULT 5,
			attempts INTEGER NOT NULL DEFAULT 0,
			status INTEGER NOT NULL DEFAULT 1,
			created_at INTEGER NOT NULL,
			FOREIGN KEY(tenant_id) REFERENCES tenants(id)
		);`,
		`CREATE TABLE IF NOT EXISTS peering_policies (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			src_tenant_id INTEGER NOT NULL,
			dst_tenant_id INTEGER NOT NULL,
			rules TEXT,
			expires_at INTEGER,
			status INTEGER NOT NULL DEFAULT 1,
			created_at INTEGER NOT NULL,
			FOREIGN KEY(src_tenant_id) REFERENCES tenants(id),
			FOREIGN KEY(dst_tenant_id) REFERENCES tenants(id)
		);`,
		`CREATE TABLE IF NOT EXISTS audit_logs (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			actor_type TEXT,
			actor_id TEXT,
			action TEXT,
			target_type TEXT,
			target_id TEXT,
			detail TEXT,
			ip TEXT,
			created_at INTEGER NOT NULL
		);`,
		`CREATE TABLE IF NOT EXISTS subnet_pool (
			subnet TEXT PRIMARY KEY,
			status INTEGER NOT NULL DEFAULT 0,
			reserved INTEGER NOT NULL DEFAULT 0,
			tenant_id INTEGER,
			updated_at INTEGER NOT NULL
		);`,
	}
	for _, stmt := range stmts {
		if _, err := s.DB.Exec(stmt); err != nil {
			return err
		}
	}
	return nil
}

func (s *Store) ensureSubnetPool() error {
	// pool: 10.10.1.0/24 .. 10.10.254.0/24
	// reserve: 10.10.0.0/24 and 10.10.255.0/24
	rows, err := s.DB.Query(`SELECT COUNT(1) FROM subnet_pool;`)
	if err != nil {
		return err
	}
	defer rows.Close()
	var count int
	if rows.Next() {
		_ = rows.Scan(&count)
	}
	if count > 0 {
		return nil
	}
	now := time.Now().Unix()
	insert := `INSERT INTO subnet_pool(subnet,status,reserved,tenant_id,updated_at) VALUES(?,?,?,?,?)`
	// reserved
	_, _ = s.DB.Exec(insert, "10.10.0.0/24", 0, 1, nil, now)
	_, _ = s.DB.Exec(insert, "10.10.255.0/24", 0, 1, nil, now)
	for i := 1; i <= 254; i++ {
		sn := fmt.Sprintf("10.10.%d.0/24", i)
		_, _ = s.DB.Exec(insert, sn, 0, 0, nil, now)
	}
	return nil
}

func (s *Store) AllocateSubnet() (string, error) {
	// find first available subnet
	row := s.DB.QueryRow(`SELECT subnet FROM subnet_pool WHERE status=0 AND reserved=0 ORDER BY subnet LIMIT 1`)
	var subnet string
	if err := row.Scan(&subnet); err != nil {
		return "", err
	}
	if subnet == "" {
		return "", errors.New("no subnet available")
	}
	return subnet, nil
}

func (s *Store) CreateTenant(name string) (*Tenant, error) {
	sn, err := s.AllocateSubnet()
	if err != nil {
		return nil, err
	}
	now := time.Now().Unix()
	res, err := s.DB.Exec(`INSERT INTO tenants(name,status,subnet,created_at) VALUES(?,?,?,?)`, name, 1, sn, now)
	if err != nil {
		return nil, err
	}
	id, _ := res.LastInsertId()
	_, _ = s.DB.Exec(`UPDATE subnet_pool SET status=1, tenant_id=?, updated_at=? WHERE subnet=?`, id, now, sn)
	return &Tenant{ID: id, Name: name, Status: 1, Subnet: sn}, nil
}

func (s *Store) CreateNodeCredential(tenantID int64, nodeName string) (*NodeCredential, error) {
	secret := randToken()
	h := hashTokenString(secret)
	res, err := s.DB.Exec(`INSERT INTO nodes(tenant_id,node_name,node_secret_hash,status) VALUES(?,?,?,1)`, tenantID, nodeName, h)
	if err != nil {
		return nil, err
	}
	id, _ := res.LastInsertId()
	return &NodeCredential{NodeID: id, NodeName: nodeName, Secret: secret, TenantID: tenantID}, nil
}

func (s *Store) VerifyNodeSecret(nodeName, secret string) (*Tenant, error) {
	h := hashTokenString(secret)
	row := s.DB.QueryRow(`SELECT t.id,t.name,t.status,t.subnet FROM nodes n JOIN tenants t ON n.tenant_id=t.id WHERE n.node_name=? AND n.node_secret_hash=? AND n.status=1`, nodeName, h)
	var t Tenant
	if err := row.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet); err != nil {
		return nil, err
	}
	return &t, nil
}

func (s *Store) GetTenantByToken(token uint64) (*Tenant, error) {
	h := hashToken(token)
	row := s.DB.QueryRow(`SELECT t.id,t.name,t.status,t.subnet FROM api_keys k JOIN tenants t ON k.tenant_id=t.id WHERE k.key_hash=? AND k.status=1`, h)
	var t Tenant
	if err := row.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet); err != nil {
		return nil, err
	}
	return &t, nil
}

func (s *Store) CreateAPIKey(tenantID int64, scope string, ttl time.Duration) (string, error) {
	token := randToken()
	h := hashTokenString(token)
	now := time.Now().Unix()
	if ttl > 0 {
		e := time.Now().Add(ttl)
		_, err := s.DB.Exec(`INSERT INTO api_keys(tenant_id,key_hash,scope,expires_at,status,created_at) VALUES(?,?,?,?,1,?)`, tenantID, h, scope, e.Unix(), now)
		return token, err
	}
	_, err := s.DB.Exec(`INSERT INTO api_keys(tenant_id,key_hash,scope,expires_at,status,created_at) VALUES(?,?,?,?,1,?)`, tenantID, h, scope, nil, now)
	return token, err
}

func (s *Store) CreateEnrollToken(tenantID int64, ttl time.Duration, maxAttempt int) (string, error) {
	code := randToken()
	h := hashTokenString(code)
	exp := time.Now().Add(ttl).Unix()
	now := time.Now().Unix()
	_, err := s.DB.Exec(`INSERT INTO enroll_tokens(tenant_id,token_hash,expires_at,max_attempt,attempts,status,created_at) VALUES(?,?,?,?,0,1,?)`, tenantID, h, exp, maxAttempt, now)
	return code, err
}

func (s *Store) ConsumeEnrollToken(code string) (*EnrollToken, error) {
	h := hashTokenString(code)
	now := time.Now().Unix()
	row := s.DB.QueryRow(`SELECT id,tenant_id,expires_at,used_at,max_attempt,attempts,status FROM enroll_tokens WHERE token_hash=?`, h)
	var et EnrollToken
	var used sql.NullInt64
	if err := row.Scan(&et.ID, &et.TenantID, &et.ExpiresAt, &used, &et.MaxAttempt, &et.Attempts, &et.Status); err != nil {
		return nil, err
	}
	if used.Valid {
		return nil, errors.New("token already used")
	}
	if et.Status != 1 {
		return nil, errors.New("token disabled")
	}
	if et.Attempts >= et.MaxAttempt {
		return nil, errors.New("token attempts exceeded")
	}
	if now > et.ExpiresAt {
		return nil, errors.New("token expired")
	}
	// mark used
	_, err := s.DB.Exec(`UPDATE enroll_tokens SET used_at=?, attempts=attempts+1 WHERE id=?`, now, et.ID)
	if err != nil {
		return nil, err
	}
	et.UsedAt = &now
	return &et, nil
}

func (s *Store) IncEnrollAttempt(code string) {
	h := hashTokenString(code)
	_, _ = s.DB.Exec(`UPDATE enroll_tokens SET attempts=attempts+1 WHERE token_hash=?`, h)
}

func hashToken(token uint64) string {
	b := make([]byte, 8)
	for i := uint(0); i < 8; i++ {
		b[7-i] = byte(token >> (i * 8))
	}
	return hashTokenBytes(b)
}

func hashTokenString(token string) string {
	return hashTokenBytes([]byte(token))
}

func (s *Store) VerifyAPIKey(token string) (*Tenant, error) {
	h := hashTokenString(token)
	row := s.DB.QueryRow(`SELECT t.id,t.name,t.status,t.subnet FROM api_keys k JOIN tenants t ON k.tenant_id=t.id WHERE k.key_hash=? AND k.status=1`, h)
	var t Tenant
	if err := row.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet); err != nil {
		return nil, err
	}
	return &t, nil
}

func hashTokenBytes(b []byte) string {
	h := sha256.Sum256(b)
	return hex.EncodeToString(h[:])
}

func randToken() string {
	b := make([]byte, 24)
	_, _ = rand.Read(b)
	return hex.EncodeToString(b)
}

// helper to avoid unused import (net)
var _ = net.IPv4len
