// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

// gen generates internal/iatadb/db.go from the OurAirports airports.csv dataset.
//
// Usage (from repo root):
//
//	go run ./internal/iatadb/gen
//
// The source CSV is read from internal/iatadb/gen/airports.csv by default.
// To use a different file, set the AIRPORTS_CSV environment variable.
// To fetch a fresh copy from OurAirports, delete airports.csv first:
//
//	rm internal/iatadb/gen/airports.csv && go run ./internal/iatadb/gen
//
// The upstream dataset is published at:
//
//	https://raw.githubusercontent.com/davidmegginson/ourairports-data/main/airports.csv
package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"sort"
	"strings"
	"text/template"
	"time"
)

const (
	sourceURL  = "https://raw.githubusercontent.com/davidmegginson/ourairports-data/main/airports.csv"
	localCSV   = "internal/iatadb/gen/airports.csv"
	outputPath = "internal/iatadb/db.go"

	colContinent = 7
	colCountry   = 8
	colIATACode  = 13
)

type entry struct {
	IATA      string
	Country   string
	Continent string
}

func main() {
	// ensure we run from the repo root
	if _, err := os.Stat("go.mod"); err != nil {
		fmt.Fprintf(os.Stderr, "error: run from repo root (go.mod not found)\n")
		os.Exit(1)
	}

	var r io.Reader

	csvPath := os.Getenv("AIRPORTS_CSV")
	if csvPath == "" {
		csvPath = localCSV
	}

	if _, err := os.Stat(csvPath); err == nil {
		f, err := os.Open(csvPath)
		if err != nil {
			log.Fatalf("open %s: %v", csvPath, err)
		}
		defer f.Close()
		r = f
		log.Printf("gen: reading from %s", csvPath)
	} else {
		log.Printf("gen: %s not found, fetching from %s", csvPath, sourceURL)
		client := &http.Client{Timeout: 30 * time.Second}
		resp, err := client.Get(sourceURL)
		if err != nil {
			log.Fatalf("fetch: %v", err)
		}
		defer resp.Body.Close()
		if resp.StatusCode != http.StatusOK {
			log.Fatalf("fetch: HTTP %d", resp.StatusCode)
		}
		// save locally for future runs
		if err := saveLocal(resp.Body, csvPath); err != nil {
			log.Printf("gen: warning: could not save local copy: %v", err)
		}
		f, err := os.Open(csvPath)
		if err != nil {
			log.Fatalf("re-open %s: %v", csvPath, err)
		}
		defer f.Close()
		r = f
	}

	cr := csv.NewReader(r)
	cr.LazyQuotes = true

	if _, err := cr.Read(); err != nil { // skip header
		log.Fatalf("read header: %v", err)
	}

	var entries []entry
	for {
		row, err := cr.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Printf("gen: skipping malformed row: %v", err)
			continue
		}
		if len(row) <= colIATACode {
			continue
		}
		iata := strings.TrimSpace(row[colIATACode])
		if len(iata) != 3 {
			continue
		}
		country := strings.TrimSpace(row[colCountry])
		continent := strings.TrimSpace(row[colContinent])
		if country == "" || continent == "" {
			continue
		}
		entries = append(entries, entry{IATA: iata, Country: country, Continent: continent})
	}

	sort.Slice(entries, func(i, j int) bool { return entries[i].IATA < entries[j].IATA })

	// deduplicate
	seen := make(map[string]struct{})
	unique := entries[:0]
	for _, e := range entries {
		if _, ok := seen[e.IATA]; !ok {
			seen[e.IATA] = struct{}{}
			unique = append(unique, e)
		}
	}
	entries = unique

	log.Printf("gen: %d valid IATA codes", len(entries))

	out, err := os.Create(outputPath)
	if err != nil {
		log.Fatalf("create %s: %v", outputPath, err)
	}
	defer out.Close()

	if err := tmpl.Execute(out, struct {
		Source  string
		Entries []entry
	}{Source: sourceURL, Entries: entries}); err != nil {
		log.Fatalf("render: %v", err)
	}
	log.Printf("gen: wrote %s", outputPath)
}

func saveLocal(r io.Reader, path string) error {
	data, err := io.ReadAll(r)
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0644)
}

var tmpl = template.Must(template.New("").Parse(`// Code generated by internal/iatadb/gen; DO NOT EDIT.
// Source: {{ .Source }}
// To regenerate: go run ./internal/iatadb/gen

package iatadb

// Entry holds geographic metadata for a single IATA airport code.
type Entry struct {
	Country   string // ISO 3166-1 alpha-2 country code e.g. "CA"
	Continent string // two-letter continent code: AF, AN, AS, EU, NA, OC, SA
}

// DB maps 3-letter IATA codes to geographic metadata.
// Generated from the OurAirports dataset; run go generate to refresh.
var DB = map[string]Entry{
{{ range .Entries }}	"{{ .IATA }}": {Country: "{{ .Country }}", Continent: "{{ .Continent }}"},
{{ end }}}

// CountryContinent maps ISO 3166-1 alpha-2 country codes to continent codes.
// Derived from the same OurAirports dataset as DB.
var CountryContinent = func() map[string]string {
	m := make(map[string]string, 250)
	for _, e := range DB {
		m[e.Country] = e.Continent
	}
	return m
}()
`))
