package main

import (
	"bytes"
	"compress/gzip"
	"crypto/sha256"
	"database/sql"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"time"

	_ "modernc.org/sqlite"
)

type Audit struct {
	db *sql.DB
}

const auditSchema = `
CREATE TABLE IF NOT EXISTS audit_log (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    ts          INTEGER NOT NULL,
    request_id  TEXT NOT NULL,
    capability  TEXT NOT NULL,
    args_hash   TEXT NOT NULL,
    exit_code   INTEGER NOT NULL,
    prev_hash   TEXT NOT NULL,
    this_hash   TEXT NOT NULL UNIQUE
);
CREATE INDEX IF NOT EXISTS audit_log_ts ON audit_log(ts);
CREATE INDEX IF NOT EXISTS audit_log_request_id ON audit_log(request_id);

CREATE TABLE IF NOT EXISTS audit_shell_eval (
    audit_id    INTEGER PRIMARY KEY,
    cmd         TEXT NOT NULL,
    cwd         TEXT,
    shell       TEXT NOT NULL,
    stdout_b64  TEXT,
    stderr_b64  TEXT,
    FOREIGN KEY (audit_id) REFERENCES audit_log(id)
);
`

func OpenAudit(path string) (*Audit, error) {
	db, err := sql.Open("sqlite", path+"?_pragma=journal_mode(wal)&_pragma=foreign_keys(on)")
	if err != nil {
		return nil, fmt.Errorf("open audit db %s: %w", path, err)
	}
	if _, err := db.Exec(auditSchema); err != nil {
		_ = db.Close()
		return nil, fmt.Errorf("apply audit schema: %w", err)
	}
	return &Audit{db: db}, nil
}

func (a *Audit) Close() error { return a.db.Close() }

// Append registra una invocacion. Devuelve this_hash.
// args raw → SHA256 → args_hash. Hash chain SHA256(prev || canonical).
func (a *Audit) Append(requestID, capability string, args any, exitCode int) (string, error) {
	argsBytes, err := json.Marshal(args)
	if err != nil {
		return "", fmt.Errorf("marshal args: %w", err)
	}
	argsHash := sha256hex(argsBytes)

	var prevHash string
	_ = a.db.QueryRow("SELECT this_hash FROM audit_log ORDER BY id DESC LIMIT 1").Scan(&prevHash)

	ts := time.Now().Unix()
	canonical := fmt.Sprintf("%s|%d|%s|%s|%s|%d", prevHash, ts, requestID, capability, argsHash, exitCode)
	thisHash := sha256hex([]byte(canonical))

	_, err = a.db.Exec(
		`INSERT INTO audit_log (ts, request_id, capability, args_hash, exit_code, prev_hash, this_hash)
		 VALUES (?, ?, ?, ?, ?, ?, ?)`,
		ts, requestID, capability, argsHash, exitCode, prevHash, thisHash,
	)
	if err != nil {
		return "", fmt.Errorf("insert audit: %w", err)
	}
	return thisHash, nil
}

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

// ShellEvalRecord is the verbose audit payload for shell.eval invocations.
// stdout / stderr larger than 4KB are gzip+base64 compressed in the DB.
type ShellEvalRecord struct {
	Cmd    string
	CWD    string
	Shell  string
	Stdout string
	Stderr string
}

// AppendVerbose registra una invocacion + payload cleartext (cmd/cwd/shell + stdout/stderr)
// para forense. Devuelve this_hash. Linkado via audit_log.id en audit_shell_eval.
func (a *Audit) AppendVerbose(requestID, capability string, args any, exitCode int, rec ShellEvalRecord) (string, error) {
	argsBytes, err := json.Marshal(args)
	if err != nil {
		return "", fmt.Errorf("marshal args: %w", err)
	}
	argsHash := sha256hex(argsBytes)

	var prevHash string
	_ = a.db.QueryRow("SELECT this_hash FROM audit_log ORDER BY id DESC LIMIT 1").Scan(&prevHash)

	ts := time.Now().Unix()
	canonical := fmt.Sprintf("%s|%d|%s|%s|%s|%d", prevHash, ts, requestID, capability, argsHash, exitCode)
	thisHash := sha256hex([]byte(canonical))

	tx, err := a.db.Begin()
	if err != nil {
		return "", fmt.Errorf("begin tx: %w", err)
	}
	defer func() { _ = tx.Rollback() }()

	res, err := tx.Exec(
		`INSERT INTO audit_log (ts, request_id, capability, args_hash, exit_code, prev_hash, this_hash)
		 VALUES (?, ?, ?, ?, ?, ?, ?)`,
		ts, requestID, capability, argsHash, exitCode, prevHash, thisHash,
	)
	if err != nil {
		return "", fmt.Errorf("insert audit_log: %w", err)
	}
	auditID, err := res.LastInsertId()
	if err != nil {
		return "", fmt.Errorf("lastinsertid: %w", err)
	}

	stdoutEnc, err := encodeMaybeGzip(rec.Stdout)
	if err != nil {
		return "", fmt.Errorf("encode stdout: %w", err)
	}
	stderrEnc, err := encodeMaybeGzip(rec.Stderr)
	if err != nil {
		return "", fmt.Errorf("encode stderr: %w", err)
	}

	if _, err := tx.Exec(
		`INSERT INTO audit_shell_eval (audit_id, cmd, cwd, shell, stdout_b64, stderr_b64)
		 VALUES (?, ?, ?, ?, ?, ?)`,
		auditID, rec.Cmd, rec.CWD, rec.Shell, stdoutEnc, stderrEnc,
	); err != nil {
		return "", fmt.Errorf("insert audit_shell_eval: %w", err)
	}

	if err := tx.Commit(); err != nil {
		return "", fmt.Errorf("commit tx: %w", err)
	}
	return thisHash, nil
}

// encodeMaybeGzip: si len(s) <= 4KB devuelve base64(plain); si supera, gzip+base64.
// Prefijo "gz:" para distinguir. Vacio devuelve "".
func encodeMaybeGzip(s string) (string, error) {
	if s == "" {
		return "", nil
	}
	const threshold = 4096
	if len(s) <= threshold {
		return "plain:" + base64.StdEncoding.EncodeToString([]byte(s)), nil
	}
	var buf bytes.Buffer
	zw := gzip.NewWriter(&buf)
	if _, err := zw.Write([]byte(s)); err != nil {
		_ = zw.Close()
		return "", err
	}
	if err := zw.Close(); err != nil {
		return "", err
	}
	return "gz:" + base64.StdEncoding.EncodeToString(buf.Bytes()), nil
}
