// Package zone manages the zone registry: each zone is a bounded slice of the
// repo with declared path globs and a ZONE.md doc. Plan §4, §11 zone discovery,
// §0 #9 write-time path enforcement.
//
// Path-based write enforcement lives in runtime.Agent (pathAllowed); this
// package owns the registry and ZONE.md parsing for assignment + lookup.
package zone

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/store"
)

// Zone is a registered zone with scope globs and a doc.
type Zone struct {
	ID         string
	Name       string
	PathGlobs  []string
	OwnerAgent string
	OwnerTeam  string
	DocPath    string
}

// Service owns the zones table.
type Service struct {
	St *store.Store
}

// New returns a service.
func New(st *store.Store) *Service { return &Service{St: st} }

// Register inserts (or updates) a zone. Validates that PathGlobs is non-empty
// (a closed-default zone scope is meaningless).
func (s *Service) Register(ctx context.Context, z Zone) error {
	if z.ID == "" {
		return errors.New("zone: id required")
	}
	if len(z.PathGlobs) == 0 {
		return errors.New("zone: at least one path glob required")
	}
	gj, _ := json.Marshal(z.PathGlobs)
	return s.St.Tx(ctx, func(q store.Querier) error {
		_, err := q.Exec(
			`INSERT INTO zones(id, name, path_globs_json, owner_agent_id, owner_team_id, doc_path)
			 VALUES(?, ?, ?, ?, ?, ?)
			 ON CONFLICT(id) DO UPDATE SET name=excluded.name, path_globs_json=excluded.path_globs_json,
			   owner_agent_id=excluded.owner_agent_id, owner_team_id=excluded.owner_team_id, doc_path=excluded.doc_path`,
			z.ID, z.Name, string(gj),
			nullable(z.OwnerAgent), nullable(z.OwnerTeam), nullable(z.DocPath),
		)
		return err
	})
}

// Lookup returns the zone for a given id.
func (s *Service) Lookup(ctx context.Context, id string) (Zone, error) {
	row := s.St.DB().QueryRowContext(ctx,
		`SELECT id, IFNULL(name,''), path_globs_json, IFNULL(owner_agent_id,''), IFNULL(owner_team_id,''), IFNULL(doc_path,'')
		   FROM zones WHERE id=?`, id,
	)
	var z Zone
	var gj string
	if err := row.Scan(&z.ID, &z.Name, &gj, &z.OwnerAgent, &z.OwnerTeam, &z.DocPath); err != nil {
		return Zone{}, err
	}
	if err := json.Unmarshal([]byte(gj), &z.PathGlobs); err != nil {
		return z, fmt.Errorf("zone: decode globs: %w", err)
	}
	return z, nil
}

// LookupByPath returns the first zone whose globs match path. If none match,
// returns ("", nil) so the caller can decide to reject the write.
func (s *Service) LookupByPath(ctx context.Context, path string) (string, error) {
	rows, err := s.St.DB().QueryContext(ctx, `SELECT id, path_globs_json FROM zones`)
	if err != nil {
		return "", err
	}
	defer rows.Close()
	for rows.Next() {
		var id, gj string
		if err := rows.Scan(&id, &gj); err != nil {
			return "", err
		}
		var globs []string
		_ = json.Unmarshal([]byte(gj), &globs)
		if Match(path, globs) {
			return id, nil
		}
	}
	return "", rows.Err()
}

// Match reports whether path is matched by any of the globs. Supports the
// "**/" prefix-extension. Same semantics as runtime.Agent.pathAllowed; we
// keep both in sync.
func Match(path string, globs []string) bool {
	clean := filepath.Clean(path)
	for _, g := range globs {
		if strings.Contains(g, "**/") {
			parts := strings.SplitN(g, "**/", 2)
			prefix := parts[0]
			suffix := parts[1]
			if strings.HasPrefix(clean, prefix) && matchSuffix(clean, suffix) {
				return true
			}
			continue
		}
		if ok, _ := filepath.Match(g, clean); ok {
			return true
		}
		if strings.HasSuffix(g, "/") && strings.HasPrefix(clean, g) {
			return true
		}
	}
	return false
}

func matchSuffix(path, suffix string) bool {
	parts := strings.Split(path, string(filepath.Separator))
	last := parts[len(parts)-1]
	ok, _ := filepath.Match(suffix, last)
	return ok
}

// EnsureDoc writes a starter ZONE.md if one doesn't exist. Used during zone
// bootstrap.
func (s *Service) EnsureDoc(z Zone) error {
	if z.DocPath == "" {
		return errors.New("zone: doc_path required to ensure doc")
	}
	if _, err := os.Stat(z.DocPath); err == nil {
		return nil
	}
	if err := os.MkdirAll(filepath.Dir(z.DocPath), 0o755); err != nil {
		return err
	}
	body := fmt.Sprintf("# Zone %s\n\nScope:\n%s\n\nOwner: %s\n",
		z.Name, formatGlobs(z.PathGlobs),
		coalesce(z.OwnerTeam, z.OwnerAgent),
	)
	return os.WriteFile(z.DocPath, []byte(body), 0o644)
}

func formatGlobs(g []string) string {
	out := make([]string, len(g))
	for i, s := range g {
		out[i] = "- " + s
	}
	return strings.Join(out, "\n")
}

func coalesce(s ...string) string {
	for _, v := range s {
		if v != "" {
			return v
		}
	}
	return "(unassigned)"
}

func nullable(s string) any {
	if s == "" {
		return nil
	}
	return s
}
