package regionmedic

import (
	"bytes"
	"compress/flate"
	"encoding/binary"
	"fmt"
	"io"
	"os"
)

// Format constants for the 7DTD ".7rg" region file.
const (
	SectorSize     = 4096
	RegionChunks   = 1024 // 32 x 32
	RegionSide     = 32
	headerSectors  = 2 // sector 0 = header, sector 1 = location table
	locTableOffset = 1 * SectorSize
	chunkConstVal  = 47
)

var (
	regionMagic = []byte{0x37, 0x72, 0x67, 0x01} // "7rg" + version 1
	chunkMagic  = []byte{0x74, 0x74, 0x63, 0x00} // "ttc\0"
)

// RegionReport is the outcome of validating a single .7rg image.
type RegionReport struct {
	Path          string     `json:"path,omitempty"`
	SizeBytes     int64      `json:"size_bytes"`
	TotalSectors  int        `json:"total_sectors"`
	PresentChunks int        `json:"present_chunks"`
	BadChunks     []BadChunk `json:"bad_chunks,omitempty"`
	// Err is set for a fatal, file-level problem (bad magic, non-sector-aligned
	// size, truncated tables) that makes the image unusable as a region at all.
	Err string `json:"err,omitempty"`
}

// Healthy reports whether the region image is structurally sound: no fatal
// error and no individually bad chunk.
func (r RegionReport) Healthy() bool { return r.Err == "" && len(r.BadChunks) == 0 }

// BadChunk describes one structurally invalid chunk within a region.
type BadChunk struct {
	Index  int    `json:"index"`
	LocalX int    `json:"local_x"`
	LocalZ int    `json:"local_z"`
	Reason string `json:"reason"`
}

// Reasons reported on BadChunk.Reason.
const (
	ReasonWrongHeader  = "wrong chunk header" // the "Wrong chunk header!" trigger
	ReasonBadConst     = "bad format constant"
	ReasonOutOfBounds  = "sector out of bounds"
	ReasonOverlap      = "overlapping sectors"
	ReasonShortRead    = "blob truncated"
	ReasonBadLength    = "implausible block length"
	ReasonDeflate      = "deflate error"
	ReasonCoordMismatch = "embedded coord mismatch"
)

// ValidateRegionFile reads path and validates it. deep=true additionally
// raw-inflates every present chunk and checks its embedded coordinates.
func ValidateRegionFile(path string, deep bool) RegionReport {
	data, err := os.ReadFile(path)
	if err != nil {
		return RegionReport{Path: path, Err: fmt.Sprintf("read: %v", err)}
	}
	r := ValidateRegionBytes(data, deep)
	r.Path = path
	return r
}

// ValidateRegionBytes validates a .7rg image held in memory.
//
// Structural checks (always): sector-aligned size; "7rg" magic; empty header
// remainder; every present chunk's location entry in-bounds and non-overlapping;
// each chunk blob's "ttc\0" magic + format constant + plausible block length.
// deep=true also inflates each chunk and verifies the decompressed payload's
// chunkX/chunkZ match the table index.
func ValidateRegionBytes(data []byte, deep bool) RegionReport {
	rep := RegionReport{SizeBytes: int64(len(data))}

	if len(data) == 0 || len(data)%SectorSize != 0 {
		rep.Err = fmt.Sprintf("size %d is not a positive multiple of %d", len(data), SectorSize)
		return rep
	}
	total := len(data) / SectorSize
	rep.TotalSectors = total
	if total < headerSectors {
		rep.Err = "file shorter than header+location table"
		return rep
	}
	if !bytes.Equal(data[0:4], regionMagic) {
		rep.Err = fmt.Sprintf("bad magic % x (want % x)", data[0:4], regionMagic)
		return rep
	}
	// Header remainder must be empty (cheap corruption tripwire).
	for _, b := range data[4:SectorSize] {
		if b != 0 {
			rep.Err = "non-zero bytes in header block"
			return rep
		}
	}

	used := make([]bool, total)
	used[0] = true // header
	used[1] = true // location table

	for idx := 0; idx < RegionChunks; idx++ {
		e := data[locTableOffset+idx*4 : locTableOffset+idx*4+4]
		off := int(e[0]) | int(e[1])<<8 | int(e[2])<<16
		cnt := int(e[3])
		if off == 0 && cnt == 0 {
			continue // absent chunk — legitimate
		}
		rep.PresentChunks++
		lx, lz := idx%RegionSide, idx/RegionSide
		bad := func(reason string) { rep.BadChunks = append(rep.BadChunks, BadChunk{Index: idx, LocalX: lx, LocalZ: lz, Reason: reason}) }

		if off < headerSectors || cnt < 1 || off+cnt > total {
			bad(ReasonOutOfBounds)
			continue
		}
		overlap := false
		for s := off; s < off+cnt; s++ {
			if used[s] {
				overlap = true
				break
			}
		}
		if overlap {
			bad(ReasonOverlap)
			continue
		}
		for s := off; s < off+cnt; s++ {
			used[s] = true
		}

		base := off * SectorSize
		capacity := cnt * SectorSize
		// Need at least the 24-byte blob header.
		if capacity < 24 {
			bad(ReasonShortRead)
			continue
		}
		blockLen := int(binary.LittleEndian.Uint32(data[base : base+4]))
		// blob occupies 16 + blockLen bytes; must fit the allocated sectors.
		if blockLen < 8 || 16+blockLen > capacity {
			bad(ReasonBadLength)
			continue
		}
		if !bytes.Equal(data[base+16:base+20], chunkMagic) {
			bad(ReasonWrongHeader) // <-- the "Wrong chunk header!" failure
			continue
		}
		if binary.LittleEndian.Uint32(data[base+20:base+24]) != chunkConstVal {
			bad(ReasonBadConst)
			continue
		}
		if deep {
			// Deep check: confirm the chunk's raw DEFLATE stream actually
			// inflates. A torn chunk whose "ttc" header happens to survive but
			// whose compressed body is garbage is caught here. The DEFLATE
			// stream is exactly blockLen-8 bytes starting at base+24.
			//
			// NOTE: we deliberately do NOT compare the decompressed payload's
			// embedded coordinates. Real pregenerated (RWG-baked) regions store
			// those in a layout that does not match a naive global-coord
			// assumption, so a coord comparison false-positives on perfectly
			// healthy regions — which would make every backup look corrupt and
			// block healing. The "ttc" magic check above is exactly what 7DTD
			// itself validates for "Wrong chunk header!", so structural validity
			// is the trustworthy signal.
			compLen := blockLen - 8
			start := base + 24
			if start+compLen > len(data) {
				bad(ReasonShortRead)
				continue
			}
			if _, err := io.ReadAll(flate.NewReader(bytes.NewReader(data[start : start+compLen]))); err != nil {
				bad(ReasonDeflate)
				continue
			}
		}
	}
	return rep
}
