package importer

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"sort"

	"github.com/SimonWaldherr/tinySQL/internal/storage"
)

// extractGeoJSONFeatures decodes GeoJSON and extracts features
func extractGeoJSONFeatures(src io.Reader) ([]map[string]any, error) {
	br := bufio.NewReader(src)
	dec := json.NewDecoder(br)

	// Decode top-level value
	var top any
	if err := dec.Decode(&top); err != nil {
		return nil, fmt.Errorf("decode geojson: %w", err)
	}

	var features []map[string]any

	// Helper to collect a single feature-like object
	addFeature := func(obj any) error {
		m, ok := obj.(map[string]any)
		if !ok {
			return fmt.Errorf("invalid feature object")
		}
		// If this is a FeatureCollection, its features are in .features
		if t, ok := m["type"].(string); ok && t == "FeatureCollection" {
			if farr, ok := m["features"].([]any); ok {
				for _, fi := range farr {
					if fm, ok := fi.(map[string]any); ok {
						features = append(features, fm)
					}
				}
				return nil
			}
		}

		// If object is a Feature, accept it
		if t, ok := m["type"].(string); ok && t == "Feature" {
			features = append(features, m)
			return nil
		}

		// If object lacks "type: Feature" but is an object with properties,
		// treat it as a single record with geometry if present.
		features = append(features, m)
		return nil
	}

	if err := addFeature(top); err != nil {
		return nil, err
	}

	// If the decoder has more JSON values (NDJSON), read them too
	for dec.More() {
		var obj any
		if err := dec.Decode(&obj); err != nil {
			break
		}
		_ = addFeature(obj)
	}

	if len(features) == 0 {
		return nil, fmt.Errorf("no features found in GeoJSON")
	}

	return features, nil
}

// buildGeoJSONColumns extracts property keys from features
func buildGeoJSONColumns(features []map[string]any) []string {
	if len(features) == 0 {
		return []string{}
	}

	seen := make(map[string]bool)
	propKeys := make([]string, 0)
	for _, feature := range features {
		if props, ok := feature["properties"].(map[string]any); ok {
			for k := range props {
				if !seen[k] {
					seen[k] = true
					propKeys = append(propKeys, k)
				}
			}
			continue
		}
		// Fallback: use top-level keys except GeoJSON structural fields.
		for k := range feature {
			if k == "geometry" || k == "type" || k == "properties" {
				continue
			}
			if !seen[k] {
				seen[k] = true
				propKeys = append(propKeys, k)
			}
		}
	}

	sort.Strings(propKeys)
	return propKeys
}

// buildGeoJSONSampleData creates sample data for type inference
func buildGeoJSONSampleData(features []map[string]any, propKeys []string) [][]string {
	sampleData := make([][]string, 0, len(features))
	for _, f := range features {
		row := make([]string, len(propKeys))
		var props map[string]any
		if p, ok := f["properties"].(map[string]any); ok {
			props = p
		} else {
			// If properties absent, try top-level keys
			props = make(map[string]any)
			for _, k := range propKeys {
				if v, ok := f[k]; ok {
					props[k] = v
				}
			}
		}
		for i, k := range propKeys {
			if v, ok := props[k]; ok && v != nil {
				row[i] = fmt.Sprintf("%v", v)
			}
		}
		sampleData = append(sampleData, row)
	}
	return sampleData
}

// extractFeatureProperties extracts properties from a GeoJSON feature
func extractFeatureProperties(f map[string]any, propKeys []string) map[string]any {
	if p, ok := f["properties"].(map[string]any); ok {
		return p
	}
	// If properties absent, try top-level keys
	props := make(map[string]any)
	for _, k := range propKeys {
		if v, ok := f[k]; ok {
			props[k] = v
		}
	}
	return props
}

// buildGeoJSONRow builds a row from a feature
func buildGeoJSONRow(f map[string]any, propKeys []string, colTypes []storage.ColType, opts *ImportOptions, rowIndex int, result *ImportResult) []any {
	row := make([]any, len(propKeys)+2)
	props := extractFeatureProperties(f, propKeys)

	for j, k := range propKeys {
		if v, ok := props[k]; ok && v != nil {
			s := fmt.Sprintf("%v", v)
			conv, err := convertValue(s, colTypes[j], opts.DateTimeFormats, opts.NullLiterals)
			if err != nil && opts.StrictTypes {
				result.Errors = append(result.Errors, fmt.Sprintf("row %d, col %s: %v", rowIndex+1, k, err))
				result.RowsSkipped++
				continue
			}
			if err != nil {
				row[j] = s
			} else {
				row[j] = conv
			}
		}
	}

	// Geometry: marshal geometry object to JSON and store as json.RawMessage
	var geom any
	if g, ok := f["geometry"]; ok && g != nil {
		geom = g
	}
	if geomMap, ok := geom.(map[string]any); ok {
		if typ, ok := geomMap["type"].(string); ok {
			row[len(row)-2] = typ
		}
	}
	if geom != nil {
		if b, err := json.Marshal(geom); err == nil {
			row[len(row)-1] = json.RawMessage(b)
		} else {
			row[len(row)-1] = nil
		}
	} else {
		row[len(row)-1] = nil
	}

	return row
}

// importGeoFeatures builds columns/types, creates or truncates the
// destination table, and inserts one row per feature. It is the shared tail
// of ImportGeoJSON and ImportTopoJSON, which differ only in how they obtain
// features.
func importGeoFeatures(ctx context.Context, db *storage.DB, tenant, tableName string, features []map[string]any, opts *ImportOptions) (*ImportResult, error) {
	propKeys := buildGeoJSONColumns(features)
	colNames := sanitizeColumnNames(append([]string{}, propKeys...))
	colNames = append(colNames, "geometry_type", "geometry")

	sampleData := buildGeoJSONSampleData(features, propKeys)

	var colTypes []storage.ColType
	if opts.TypeInference {
		colTypes = inferColumnTypes(sampleData, len(propKeys), opts)
	} else {
		colTypes = make([]storage.ColType, len(propKeys))
		for i := range colTypes {
			colTypes[i] = storage.TextType
		}
	}
	colTypes = append(colTypes, storage.TextType, storage.GeometryType)

	result := &ImportResult{Encoding: "utf-8", Errors: make([]string, 0), ColumnNames: colNames, ColumnTypes: colTypes}

	if opts.CreateTable {
		if err := createTable(ctx, db, tenant, tableName, colNames, colTypes); err != nil {
			return nil, err
		}
	}
	if opts.Truncate {
		if err := truncateTable(ctx, db, tenant, tableName); err != nil {
			return nil, err
		}
	}

	tbl, err := db.Get(tenant, tableName)
	if err != nil {
		return nil, fmt.Errorf("get table: %w", err)
	}

	for i, f := range features {
		row := buildGeoJSONRow(f, propKeys, colTypes, opts, i, result)
		tbl.Rows = append(tbl.Rows, row)
		result.RowsInserted++
	}

	return result, nil
}

// ImportGeoJSON imports a GeoJSON file. It supports FeatureCollection and
// individual Feature objects. Properties become table columns; geometry is
// stored in a `geometry` column with type GeometryType.
func ImportGeoJSON(
	ctx context.Context,
	db *storage.DB,
	tenant string,
	tableName string,
	src io.Reader,
	opts *ImportOptions,
) (*ImportResult, error) {
	if opts == nil {
		opts = &ImportOptions{}
	}
	applyDefaults(opts)

	features, err := extractGeoJSONFeatures(src)
	if err != nil {
		return nil, err
	}
	return importGeoFeatures(ctx, db, tenant, tableName, features, opts)
}

// marshalFeaturesAndImportGeoJSON wraps features as a GeoJSON FeatureCollection
// and imports it via ImportGeoJSON. Shared by the KML and Shapefile importers,
// which build features via different decoders but converge on this same tail.
func marshalFeaturesAndImportGeoJSON(ctx context.Context, db *storage.DB, tenant, tableName string, features []map[string]any, opts *ImportOptions) (*ImportResult, error) {
	fc := map[string]any{"type": "FeatureCollection", "features": features}
	b, err := json.Marshal(fc)
	if err != nil {
		return nil, fmt.Errorf("marshal featurecollection: %w", err)
	}
	return ImportGeoJSON(ctx, db, tenant, tableName, bytes.NewReader(b), opts)
}
