// HOLOCRON 2.0.1 holocron.go
// Network Intelligence Console
// K0NxT3D 2025

package main

import (
	"bytes"
	"database/sql"
	"encoding/csv"
	"encoding/json"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	//"os"
	"os/exec"
	"runtime"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	_ "github.com/mattn/go-sqlite3"
)

const (
	DB_FILE = "asn.db"
	PORT    = "35001"
)

// Template: converted from your HTML (Jinja -> Go templates)
var HTML_TEMPLATE = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Holocron Console v2.0 — K0NxT3D</title>

<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

<!-- Sith Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Cinzel:wght@400;700&display=swap" rel="stylesheet">

<style>
/* ========= HOLONCRON: DARK SITH / TEMPLAR THEME ========= */
/* color variables */
:root{
  --bg-0:#050405;       /* page background */
  --bg-1:#08060a;       /* panel backgrounds */
  --panel-edge:#2b0000;
  --accent:#b65a2a;     /* ancient-gold warm accent */
  --sith-red:#9b0f0f;   /* primary red */
  --muted:#a9a2a2;      /* secondary text */
  --bright:#e8e6e6;     /* primary text */
  --glass: rgba(255,0,0,0.05);
}

/* page */
html,body{
  height:100%;
  margin:0;
  background: radial-gradient(ellipse at center, var(--bg-0) 0%, #000 75%);
  color:var(--bright);
  font-family: 'Orbitron', 'Cinzel', monospace;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
}

/* subtle scanlines but very dark so no white */
body::before{
  content:"";
  position:fixed; inset:0;
  background: repeating-linear-gradient(
    to bottom,
    rgba(155,15,15,0.015) 0px,
    rgba(155,15,15,0.015) 1px,
    transparent 2px,
    transparent 3px
  );
  pointer-events:none;
  z-index:0;
}

/* main panel */
.panel{
  background: linear-gradient(180deg, rgba(10,6,6,0.9), rgba(2,2,2,0.95));
  border:1px solid rgba(155,15,15,0.35);
  box-shadow: 0 8px 40px rgba(0,0,0,0.8), inset 0 0 18px rgba(155,15,15,0.04);
  border-radius:12px;
  padding:20px;
  margin-top:18px;
  z-index:1;
}

/* header */
h1{
  color:var(--accent);
  letter-spacing:3px;
  margin:0;
  font-weight:800;
  text-shadow: 0 0 10px rgba(155,15,15,0.12);
}
.subhead{ color: #aa8f8a; font-size:0.9rem; }

/* buttons */
.btn-primary{
  background: linear-gradient(180deg, var(--sith-red), #2b0000);
  border:1px solid rgba(120,20,20,0.6);
  color:var(--bright);
  text-transform:uppercase;
  letter-spacing:1px;
  box-shadow: 0 6px 18px rgba(155,15,15,0.12);
}
.btn-primary:hover{
  background: linear-gradient(180deg, #bf1f1f, #3a0000);
  box-shadow: 0 8px 28px rgba(190,20,20,0.18);
}
.btn-ghost{
  background:transparent;
  border:1px solid rgba(155,15,15,0.2);
  color:var(--accent);
}

/* form controls */
.form-control, .form-select {
  background: linear-gradient(180deg, #070606, #0b0707);
  border:1px solid rgba(120,20,20,0.45);
  color:var(--bright);
  box-shadow: inset 0 0 8px rgba(0,0,0,0.6);
}
.form-control::placeholder{ color: #7e6d6d; }
.form-control:focus, .form-select:focus {
  border-color: var(--sith-red);
  box-shadow: 0 0 10px rgba(155,15,15,0.12);
  outline: none;
}

/* Dropdown menu background (Bootstrap resets require this) */
.form-select option {
    background-color: #000 !important;
    color: #d9d3d3 !important;
}

/* Prevent white flash when opening the dropdown */
select::-ms-expand { display: none; }

/* table */
.table {
  background: transparent;
  color: var(--bright);
}
.table thead th {
  background: linear-gradient(180deg, rgba(140,10,10,0.14), rgba(120,10,10,0.06));
  color: var(--accent);
  border-bottom:1px solid rgba(155,15,15,0.18);
}
.table tbody tr {
  background: linear-gradient(90deg, rgba(0,0,0,0.35), rgba(0,0,0,0.05));
}
.table td { background-color: transparent; color: var(--bright); vertical-align: middle; }

/* console log */
#console-log {
  background: linear-gradient(180deg, rgba(5,0,0,0.45), rgba(0,0,0,0.6));
  border:1px solid rgba(120,20,20,0.2);
  padding:12px;
  border-radius:8px;
  color: var(--accent);
  font-family: monospace;
  height:120px;
  overflow:auto;
}

/* prefix badges */
.prefix-badge {
  display:inline-block;
  background: rgba(155,15,15,0.12);
  border:1px solid rgba(155,15,15,0.28);
  color: var(--bright);
  border-radius:6px;
  padding:6px 10px;
  margin:6px 6px 6px 0;
  font-family:'Orbitron', monospace;
  font-size:0.85rem;
  white-space:nowrap;
  max-width:100%;
  overflow:hidden;
  text-overflow:ellipsis;
}

/* Modal tweaks -- ensure modal fits viewport and body scrolls internally */
.modal-backdrop.show { background-color: rgba(0,0,0,0.65); }
.modal-content {
  background: linear-gradient(180deg, #070505, #0a0606);
  border:1px solid rgba(155,15,15,0.28);
  color:var(--bright);
  box-shadow: 0 12px 48px rgba(0,0,0,0.8);
}

/* Make modal dialog responsive and never exceed viewport */
.modal-dialog {
  max-width: 940px;
  margin: 1.5rem auto;
}

/* Modal header fixes */
.modal-header {
  border-bottom: 1px solid rgba(155,15,15,0.08);
  position: sticky;
  top: 0;
  z-index: 3;
  background: linear-gradient(180deg, rgba(10,6,6,0.96), rgba(8,4,4,0.96));
}

/* Ensure modal body has internal scrolling when content is long */
.modal-body {
  max-height: calc(80vh - 120px);
  overflow-y: auto;
  padding: 1rem 1.25rem;
  background: transparent;
}

/* keep close button visible on dark background */
.btn-close {
  filter: invert(1) brightness(1.3);
  opacity: 0.95;
  outline: none;
}

/* small devices - modal full width with padding */
@media (max-width: 576px) {
  .modal-dialog { max-width: 96%; margin: 0.75rem; }
  .modal-body { max-height: calc(80vh - 100px); }
}

/* remove any accidental white background from images or svg */
img, svg { background: transparent !important; }

/* custom utility */
.text-muted-dark { color: #7f6b6b !important; }
</style>
</head>
<body>
<div class="container">
    <div class="panel">
        <div class="d-flex justify-content-between align-items-center mb-2">
            <div>
                <h1>HOLOCRON</h1>
                <div class="subhead">Holocron Data Archive Interface</div>
            </div>
            <button class="btn btn-ghost" id="theme-hint">SYSTEM MODE: ARCHIVE</button>
        </div>

        <form method="POST" class="search-form row g-3 align-items-center">
            <div class="col-md-6">
                <input type="text" class="form-control" name="search_term" placeholder="Enter search term (ASN, name, location...)" value="{{ .SearchTerm }}">
            </div>
            <div class="col-md-3">
                <select class="form-select" name="search_by">
                    <option value="ASN" {{if eq .SearchBy "ASN"}}selected{{end}}>ASN</option>
                    <option value="Name" {{if eq .SearchBy "Name"}}selected{{end}}>Name</option>
                    <option value="Type" {{if eq .SearchBy "Type"}}selected{{end}}>Type</option>
                    <option value="Location" {{if eq .SearchBy "Location"}}selected{{end}}>Location</option>
                </select>
            </div>
            <div class="col-md-3">
                <button type="submit" class="btn btn-primary w-100">SEARCH</button>
            </div>
        </form>

        {{if .HasResults}}
        <hr style="border-color: rgba(155,15,15,0.16)">
        <h5 style="color:#bb9999;">Results</h5>
        <div class="table-responsive">
            <table class="table table-borderless">
                <thead>
                    <tr>
                        <th>ASN</th>
                        <th>Name</th>
                        <th>Type</th>
                        <th>Location</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    {{range .Results}}
                    <tr>
                        <td>{{.ASN}}</td>
                        <td>{{.Name}}</td>
                        <td>{{.Type}}</td>
                        <td>{{.Location}}</td>
                        <td><button class="btn btn-primary btn-sm" onclick="openRangesModal('{{.ASN}}')">Ranges</button></td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
        </div>

        <form method="POST" action="/export_csv" class="mt-3">
            <input type="hidden" name="search_term" value="{{ .SearchTerm }}">
            <input type="hidden" name="search_by" value="{{ .SearchBy }}">
            <button type="submit" class="btn btn-primary">Export Table CSV</button>
        </form>
        {{else}}
            {{if .Searched}}
                <hr style="border-color: rgba(155,15,15,0.12)">
                <div class="text-center text-muted-dark mt-3">No results found.</div>
            {{end}}
        {{end}}

        <div id="console-log">
            <span>[INIT] Holocron systems active.</span>
            <span>[READY] Awaiting command input...</span>
        </div>

        <footer class="small mt-3">Holocron Console v2.0 — <span style="color:var(--accent)">K0NxT3D</span></footer>
    </div>
</div>

<!-- Modal -->
<div class="modal fade" id="rangesModal" tabindex="-1" aria-labelledby="rangesModalLabel" aria-hidden="true">
  <div class="modal-dialog modal-lg modal-dialog-centered" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="rangesModalLabel">IP Ranges</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <div id="ranges-loading" class="text-muted-dark">Querying the database archives...</div>
        <div id="ranges-list" class="mt-3" style="display:flex;flex-wrap:wrap;gap:6px;"></div>
        <div id="ranges-empty" class="text-muted-dark" style="display:none">No data found.</div>

        <form id="exportRangesForm" method="POST" action="/export_ranges_csv" target="_blank" class="mt-3">
            <input type="hidden" name="asn" id="export-asn">
            <button type="submit" class="btn btn-primary">Export Ranges CSV</button>
        </form>
      </div>
    </div>
  </div>
</div>

<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const modalEl = document.getElementById('rangesModal');
const bsModal = new bootstrap.Modal(modalEl, { keyboard: true });
const consoleLog = document.getElementById('console-log');

function logConsole(msg) {
    const span = document.createElement('span');
    span.textContent = msg;
    consoleLog.appendChild(span);
    consoleLog.scrollTop = consoleLog.scrollHeight;
}

function openRangesModal(asn) {
    document.getElementById('ranges-loading').style.display = 'block';
    document.getElementById('ranges-list').innerHTML = '';
    document.getElementById('ranges-empty').style.display = 'none';
    document.getElementById('export-asn').value = asn;
    bsModal.show();

    let normalized = asn.toString().trim();
    if (!/^AS/i.test(normalized)) normalized = 'AS' + normalized;

    fetch('/ranges_json/' + encodeURIComponent(normalized))
        .then(r => r.json())
        .then(data => {
            document.getElementById('ranges-loading').style.display = 'none';
            const list = document.getElementById('ranges-list');
            if (!data.prefixes || !data.prefixes.length) {
                document.getElementById('ranges-empty').style.display = 'block';
                logConsole('[WARN] No ranges found for ' + normalized);
                return;
            }
            data.prefixes.forEach(p => {
                const el = document.createElement('span');
                el.className = 'prefix-badge';
                el.textContent = p;
                list.appendChild(el);
            });
            logConsole('[INFO] ' + data.prefixes.length + ' prefixes retrieved for ' + normalized);
        })
        .catch(err => {
            console.error(err);
            document.getElementById('ranges-loading').textContent = 'Error fetching data.';
            logConsole('[ERROR] Failed to retrieve ranges for ' + asn);
        });
}
</script>
</body>
</html>
`

// ASNRecord is a single row from asn table
type ASNRecord struct {
	ASN      string
	Name     string
	Type     string
	Location string
}

// TemplateData is passed to HTML template
type TemplateData struct {
	Results    []ASNRecord
	HasResults bool
	SearchTerm string
	SearchBy   string
	Searched   bool
}

func main() {
	// Use Gin
	gin.SetMode(gin.ReleaseMode)
	r := gin.Default()

	// Parse template with eq func
	tmpl := template.Must(template.New("holocron").Funcs(template.FuncMap{
		"eq": func(a, b string) bool { return a == b },
	}).Parse(HTML_TEMPLATE))

	// Home GET and POST
	r.GET("/", func(c *gin.Context) {
		data := TemplateData{Results: nil, HasResults: false, SearchTerm: "", SearchBy: "ASN", Searched: false}
		c.Status(http.StatusOK)
		c.Header("Content-Type", "text/html; charset=utf-8")
		if err := tmpl.Execute(c.Writer, data); err != nil {
			log.Println("template exec error:", err)
		}
	})

	r.POST("/", func(c *gin.Context) {
		searchTerm := strings.TrimSpace(c.PostForm("search_term"))
		searchBy := c.DefaultPostForm("search_by", "ASN")
		results, err := queryDatabase(searchTerm, searchBy)
		if err != nil {
			log.Println("db query error:", err)
			c.String(http.StatusInternalServerError, "DB error")
			return
		}
		data := TemplateData{Results: results, HasResults: len(results) > 0, SearchTerm: searchTerm, SearchBy: searchBy, Searched: true}
		c.Status(http.StatusOK)
		c.Header("Content-Type", "text/html; charset=utf-8")
		if err := tmpl.Execute(c.Writer, data); err != nil {
			log.Println("template exec error:", err)
		}
	})

	// Legacy direct ranges page
	r.GET("/ranges/:asn", func(c *gin.Context) {
		asn := c.Param("asn")
		prefixes, _ := fetchPrefixes(asn)
		var b bytes.Buffer
		b.WriteString("<h1>Prefixes for " + template.HTMLEscapeString(asn) + "</h1><ul>")
		for _, p := range prefixes {
			b.WriteString("<li>" + template.HTMLEscapeString(p) + "</li>")
		}
		b.WriteString("</ul><a href='/'>Back</a>")
		c.Data(http.StatusOK, "text/html; charset=utf-8", b.Bytes())
	})

	// JSON endpoint for modal
	r.GET("/ranges_json/:asn", func(c *gin.Context) {
		asn := c.Param("asn")
		prefixes, _ := fetchPrefixes(asn)
		c.JSON(http.StatusOK, gin.H{"asn": asn, "prefixes": prefixes})
	})

	// Export CSV for search results
	r.POST("/export_csv", func(c *gin.Context) {
		searchTerm := strings.TrimSpace(c.PostForm("search_term"))
		searchBy := c.DefaultPostForm("search_by", "ASN")
		results, err := queryDatabase(searchTerm, searchBy)
		if err != nil {
			log.Println("db query error:", err)
			c.String(http.StatusInternalServerError, "DB error")
			return
		}
		// Build CSV
		var buf bytes.Buffer
		w := csv.NewWriter(&buf)
		w.Write([]string{"ASN", "Name", "Type", "Location"})
		for _, r := range results {
			w.Write([]string{r.ASN, r.Name, r.Type, r.Location})
		}
		w.Flush()
		c.Header("Content-Disposition", "attachment; filename=asn_results.csv")
		c.Data(http.StatusOK, "text/csv", buf.Bytes())
	})

	// Export CSV for prefixes
	r.POST("/export_ranges_csv", func(c *gin.Context) {
		asn := strings.TrimSpace(c.PostForm("asn"))
		if asn != "" && !strings.HasPrefix(strings.ToUpper(asn), "AS") {
			asn = "AS" + asn
		}
		prefixes, _ := fetchPrefixes(asn)
		var buf bytes.Buffer
		w := csv.NewWriter(&buf)
		w.Write([]string{"ASN", "Prefix"})
		for _, p := range prefixes {
			w.Write([]string{asn, p})
		}
		w.Flush()
		filename := asn + "_prefixes.csv"
		c.Header("Content-Disposition", "attachment; filename="+filename)
		c.Data(http.StatusOK, "text/csv", buf.Bytes())
	})

	// Start server and open browser
	go func() {
		time.Sleep(time.Second)
		openBrowser("http://127.0.0.1:" + PORT)
	}()

	log.Println("Starting Holocron on port", PORT)
	if err := r.Run(":" + PORT); err != nil {
		log.Fatalln("Failed to run server:", err)
	}
}

// queryDatabase searches the sqlite DB
func queryDatabase(searchTerm, searchBy string) ([]ASNRecord, error) {
	db, err := sql.Open("sqlite3", DB_FILE)
	if err != nil {
		return nil, err
	}
	defer db.Close()

	var query string
	switch searchBy {
	case "ASN":
		query = "SELECT asn, name, type, location FROM asn WHERE asn LIKE ?"
	case "Name":
		query = "SELECT asn, name, type, location FROM asn WHERE name LIKE ?"
	case "Type":
		query = "SELECT asn, name, type, location FROM asn WHERE type LIKE ?"
	case "Location":
		query = "SELECT asn, name, type, location FROM asn WHERE location LIKE ?"
	default:
		return []ASNRecord{}, nil
	}

	like := "%" + searchTerm + "%"
	rows, err := db.Query(query, like)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	out := make([]ASNRecord, 0)
	for rows.Next() {
		var r ASNRecord
		if err := rows.Scan(&r.ASN, &r.Name, &r.Type, &r.Location); err != nil {
			log.Println("row scan err:", err)
			continue
		}
		out = append(out, r)
	}
	return out, nil
}

// fetchPrefixes calls RIPE stat API
func fetchPrefixes(asn string) ([]string, error) {
	if asn == "" {
		return []string{}, nil
	}
	url := "https://stat.ripe.net/data/announced-prefixes/data.json?resource=" + asn
	client := &http.Client{Timeout: 12 * time.Second}
	resp, err := client.Get(url)
	if err != nil {
		return []string{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return []string{}, nil
	}
	body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 5<<20)) // limit 5MB
	if err != nil {
		return []string{}, err
	}
	var parsed struct {
		Data struct {
			Prefixes []struct {
				Prefix string `json:"prefix"`
			} `json:"prefixes"`
		} `json:"data"`
	}
	if err := json.Unmarshal(body, &parsed); err != nil {
		return []string{}, err
	}
	out := make([]string, 0, len(parsed.Data.Prefixes))
	for _, p := range parsed.Data.Prefixes {
		out = append(out, p.Prefix)
	}
	return out, nil
}

// openBrowser opens the default browser for the given URL across OSes
func openBrowser(url string) {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "linux":
		cmd = exec.Command("xdg-open", url)
	case "darwin":
		cmd = exec.Command("open", url)
	case "windows":
		cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
	default:
		log.Println("unsupported platform; open the browser manually:", url)
		return
	}
	// ignore error if command can't run
	if err := cmd.Start(); err != nil {
		log.Println("failed to open browser:", err)
	}
}

