package kb

import (
	"context"
	"crypto/sha1" // #nosec G505 -- verifies legacy Git object identity only.
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	pathpkg "path"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/agsbridge"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
)

const maxKBSourceMigrationMetadataBytes = int64(8 << 20)

type KBSourceContentExportRequest struct {
	Repository      repos.RepositoryIdentity
	CanonicalBranch string
	CommitSHA       string
	Limits          contentpayload.Limits
}

type KBSourceContentExportFile struct {
	Path       string
	Data       []byte
	Executable bool
}

type KBSourceContentExport struct {
	Files             []KBSourceContentExportFile
	RootDigest        string
	SizeBytes         int64
	SourceCommittedAt time.Time
}

type KBSourceContentExporter interface {
	ExportExactKBSource(context.Context, KBSourceContentExportRequest) (KBSourceContentExport, error)
}

// AGSExactKBSourceExporter is the KB-owned legacy read seam. It uses only
// existing repository/tree/blob APIs and never changes AGS state. Each call is
// bounded by the caller's file, byte, manifest, and outer time limits.
type AGSExactKBSourceExporter struct {
	BaseURL             string
	Handler             http.Handler
	AuthHeader          string
	AuthorizationHeader string
	HTTPClient          *http.Client
}

func (exporter AGSExactKBSourceExporter) ExportExactKBSource(ctx context.Context, request KBSourceContentExportRequest) (KBSourceContentExport, error) {
	repository, branch, commit, limits, err := normalizeKBSourceContentExportRequest(request)
	if err != nil {
		return KBSourceContentExport{}, err
	}
	plan, err := exporter.preflightExactKBSource(ctx, repository, branch, commit, limits)
	if err != nil {
		return KBSourceContentExport{}, err
	}
	return exporter.readExactKBSource(ctx, repository, plan, limits)
}

func normalizeKBSourceContentExportRequest(request KBSourceContentExportRequest) (repos.RepositoryIdentity, string, string, contentpayload.Limits, error) {
	repository := request.Repository
	repository.ID = strings.TrimSpace(repository.ID)
	repository.Owner = strings.TrimSpace(repository.Owner)
	repository.Name = strings.TrimSpace(repository.Name)
	repository.FullName = strings.TrimSpace(repository.FullName)
	if repository.FullName == "" && repository.Owner != "" && repository.Name != "" {
		repository.FullName = repository.Owner + "/" + repository.Name
	}
	branch := strings.TrimSpace(request.CanonicalBranch)
	commit := strings.ToLower(strings.TrimSpace(request.CommitSHA))
	limits, err := contentpayload.NormalizeLimits(request.Limits)
	if err != nil {
		return repository, branch, commit, limits, err
	}
	if repository.ID == "" || repository.FullName == "" || branch == "" || !fullKBSourceCommitSHA(commit) || limits != request.Limits {
		return repository, branch, commit, limits, fmt.Errorf("exact KB source export target and normalized bounds are required")
	}
	owner, name, ok := strings.Cut(repository.FullName, "/")
	if !ok || strings.TrimSpace(owner) == "" || strings.TrimSpace(name) == "" || strings.Contains(name, "/") {
		return repository, branch, commit, limits, fmt.Errorf("exact KB source repository owner and name are required")
	}
	repository.Owner, repository.Name = strings.TrimSpace(owner), strings.TrimSpace(name)
	return repository, branch, commit, limits, nil
}

type kbSourceExportPlanEntry struct {
	Path       string
	BlobSHA    string
	SizeBytes  int64
	Executable bool
}

type kbSourceExportPlan struct {
	entries           []kbSourceExportPlanEntry
	sourceCommittedAt time.Time
}

func (exporter AGSExactKBSourceExporter) preflightExactKBSource(ctx context.Context, repository repos.RepositoryIdentity, branch, commit string, limits contentpayload.Limits) (kbSourceExportPlan, error) {
	base := "/api/v3/repos/" + url.PathEscape(repository.Owner) + "/" + url.PathEscape(repository.Name)
	repoBody, err := exporter.readBoundedAGSResponse(ctx, base, 64<<10)
	if err != nil {
		return kbSourceExportPlan{}, fmt.Errorf("inspect KB source migration repository: %w", err)
	}
	var live struct {
		ID            json.RawMessage `json:"id"`
		DefaultBranch string          `json:"default_branch"`
	}
	if err := json.Unmarshal(repoBody, &live); err != nil {
		return kbSourceExportPlan{}, fmt.Errorf("decode KB source migration repository: %w", err)
	}
	liveID, err := rawJSONIdentity(live.ID)
	if err != nil || liveID != repository.ID || strings.TrimSpace(live.DefaultBranch) != branch {
		return kbSourceExportPlan{}, fmt.Errorf("KB source migration live repository identity or canonical branch changed")
	}
	commitBody, err := exporter.readBoundedAGSResponse(ctx, base+"/git/commits/"+url.PathEscape(commit), 128<<10)
	if err != nil {
		return kbSourceExportPlan{}, fmt.Errorf("inspect KB source migration commit: %w", err)
	}
	var commitResponse struct {
		SHA    string `json:"sha"`
		Author struct {
			Date string `json:"date"`
		} `json:"author"`
		Committer struct {
			Date string `json:"date"`
		} `json:"committer"`
	}
	if err := json.Unmarshal(commitBody, &commitResponse); err != nil {
		return kbSourceExportPlan{}, fmt.Errorf("decode KB source migration commit: %w", err)
	}
	committedAt, err := time.Parse(time.RFC3339, firstNonEmptyString(commitResponse.Committer.Date, commitResponse.Author.Date))
	if err != nil || !strings.EqualFold(strings.TrimSpace(commitResponse.SHA), commit) {
		return kbSourceExportPlan{}, fmt.Errorf("KB source migration commit identity or time changed")
	}
	metadataLimit := limits.MaxManifestBytes
	if metadataLimit < 64<<10 {
		metadataLimit = 64 << 10
	}
	if metadataLimit > maxKBSourceMigrationMetadataBytes {
		metadataLimit = maxKBSourceMigrationMetadataBytes
	}
	treeBody, err := exporter.readBoundedAGSResponse(ctx, base+"/git/trees/"+url.PathEscape(commit)+"?recursive=1", metadataLimit)
	if err != nil {
		return kbSourceExportPlan{}, fmt.Errorf("inspect KB source migration tree: %w", err)
	}
	var treeResponse struct {
		SHA       string `json:"sha"`
		Truncated bool   `json:"truncated"`
		Tree      []struct {
			Path string `json:"path"`
			Mode string `json:"mode"`
			Type string `json:"type"`
			SHA  string `json:"sha"`
			Size int64  `json:"size"`
		} `json:"tree"`
	}
	if err := json.Unmarshal(treeBody, &treeResponse); err != nil {
		return kbSourceExportPlan{}, fmt.Errorf("decode KB source migration tree: %w", err)
	}
	if treeResponse.Truncated {
		return kbSourceExportPlan{}, fmt.Errorf("KB source migration tree response is truncated")
	}
	entries := make([]kbSourceExportPlanEntry, 0, len(treeResponse.Tree))
	seen := make(map[string]struct{}, len(treeResponse.Tree))
	var total int64
	for _, entry := range treeResponse.Tree {
		if err := ctx.Err(); err != nil {
			return kbSourceExportPlan{}, err
		}
		kind, mode := strings.ToLower(strings.TrimSpace(entry.Type)), strings.TrimSpace(entry.Mode)
		if kind == "tree" && mode == "040000" {
			continue
		}
		if kind != "blob" || (mode != "100644" && mode != "100755") {
			return kbSourceExportPlan{}, fmt.Errorf("KB source migration tree contains symlink, gitlink, or special entry")
		}
		path, err := cleanKBSourceExportPath(entry.Path)
		if err != nil || !fullKBSourceCommitSHA(strings.ToLower(strings.TrimSpace(entry.SHA))) || entry.Size < 0 || entry.Size > limits.MaxFileBytes || len(entries) >= limits.MaxFiles || total > limits.MaxTotalBytes-entry.Size {
			return kbSourceExportPlan{}, fmt.Errorf("KB source migration tree exceeds path, file, or byte bounds")
		}
		if _, duplicate := seen[path]; duplicate {
			return kbSourceExportPlan{}, fmt.Errorf("KB source migration tree contains duplicate path")
		}
		entries = append(entries, kbSourceExportPlanEntry{Path: path, BlobSHA: strings.ToLower(strings.TrimSpace(entry.SHA)), SizeBytes: entry.Size, Executable: mode == "100755"})
		seen[path] = struct{}{}
		total += entry.Size
	}
	if len(entries) == 0 {
		return kbSourceExportPlan{}, fmt.Errorf("KB source migration tree is empty")
	}
	sort.Slice(entries, func(i, j int) bool { return entries[i].Path < entries[j].Path })
	return kbSourceExportPlan{entries: entries, sourceCommittedAt: committedAt.UTC()}, nil
}

func (exporter AGSExactKBSourceExporter) readExactKBSource(ctx context.Context, repository repos.RepositoryIdentity, plan kbSourceExportPlan, limits contentpayload.Limits) (KBSourceContentExport, error) {
	base := "/api/v3/repos/" + url.PathEscape(repository.Owner) + "/" + url.PathEscape(repository.Name) + "/git/blobs/"
	files := make([]KBSourceContentExportFile, 0, len(plan.entries))
	manifest := make([]contentpayload.ManifestEntry, 0, len(plan.entries))
	var total int64
	for _, planned := range plan.entries {
		if err := ctx.Err(); err != nil {
			return KBSourceContentExport{}, err
		}
		responseLimit := planned.SizeBytes*4/3 + (64 << 10)
		body, err := exporter.readBoundedAGSResponse(ctx, base+url.PathEscape(planned.BlobSHA), responseLimit)
		if err != nil {
			return KBSourceContentExport{}, fmt.Errorf("read KB source migration blob %q: %w", planned.Path, err)
		}
		var blob struct {
			SHA      string `json:"sha"`
			Size     int64  `json:"size"`
			Encoding string `json:"encoding"`
			Content  string `json:"content"`
		}
		if err := json.Unmarshal(body, &blob); err != nil {
			return KBSourceContentExport{}, fmt.Errorf("decode KB source migration blob %q: %w", planned.Path, err)
		}
		if !strings.EqualFold(strings.TrimSpace(blob.SHA), planned.BlobSHA) || blob.Size != planned.SizeBytes || strings.ToLower(strings.TrimSpace(blob.Encoding)) != "base64" {
			return KBSourceContentExport{}, fmt.Errorf("KB source migration blob %q changed after preflight", planned.Path)
		}
		data, err := base64.StdEncoding.DecodeString(strings.Map(func(r rune) rune {
			if r == '\r' || r == '\n' {
				return -1
			}
			return r
		}, blob.Content))
		if err != nil || int64(len(data)) != planned.SizeBytes || int64(len(data)) > limits.MaxFileBytes || total > limits.MaxTotalBytes-int64(len(data)) {
			return KBSourceContentExport{}, fmt.Errorf("KB source migration blob %q exceeds or changes declared bounds", planned.Path)
		}
		if gitBlobSHA(data) != planned.BlobSHA {
			return KBSourceContentExport{}, fmt.Errorf("KB source migration blob %q digest changed", planned.Path)
		}
		sum := sha256.Sum256(data)
		digest := hex.EncodeToString(sum[:])
		files = append(files, KBSourceContentExportFile{Path: planned.Path, Data: data, Executable: planned.Executable})
		manifest = append(manifest, contentpayload.ManifestEntry{Path: planned.Path, Digest: contentpayload.Digest(digest), SizeBytes: int64(len(data)), Executable: planned.Executable})
		total += int64(len(data))
	}
	encoded, err := json.Marshal(manifest)
	if err != nil {
		return KBSourceContentExport{}, err
	}
	if int64(len(encoded)) > limits.MaxManifestBytes {
		return KBSourceContentExport{}, fmt.Errorf("KB source migration manifest exceeds byte limit")
	}
	digest := sha256.Sum256(encoded)
	return KBSourceContentExport{Files: files, RootDigest: hex.EncodeToString(digest[:]), SizeBytes: total, SourceCommittedAt: plan.sourceCommittedAt}, nil
}

func (exporter AGSExactKBSourceExporter) readBoundedAGSResponse(ctx context.Context, path string, limit int64) ([]byte, error) {
	if limit <= 0 || limit > KBSourceMigrationMaxFileBytes*2+maxKBSourceMigrationMetadataBytes {
		return nil, fmt.Errorf("invalid KB source migration response limit")
	}
	if exporter.Handler != nil {
		request := httptest.NewRequest(http.MethodGet, path, nil).WithContext(agsbridge.ContextWithSiteAdmin(ctx))
		exporter.setAGSAuthorization(request.Header)
		response := newBoundedKBSourceMigrationResponse(limit)
		exporter.Handler.ServeHTTP(response, request)
		return response.result()
	}
	baseURL := strings.TrimRight(strings.TrimSpace(exporter.BaseURL), "/")
	parsed, err := url.Parse(baseURL)
	if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
		return nil, fmt.Errorf("valid AGS API base URL is required")
	}
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+path, nil)
	if err != nil {
		return nil, err
	}
	exporter.setAGSAuthorization(request.Header)
	client := exporter.HTTPClient
	if client == nil {
		client = http.DefaultClient
	}
	response, err := client.Do(request)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4<<10))
		return nil, fmt.Errorf("AGS repository response status %d", response.StatusCode)
	}
	body, err := io.ReadAll(io.LimitReader(response.Body, limit+1))
	if err != nil {
		return nil, err
	}
	if int64(len(body)) > limit {
		return nil, fmt.Errorf("AGS repository response exceeds byte limit")
	}
	return body, nil
}

func (exporter AGSExactKBSourceExporter) setAGSAuthorization(header http.Header) {
	name := strings.TrimSpace(exporter.AuthHeader)
	if name == "" {
		name = "Authorization"
	}
	if value := strings.TrimSpace(exporter.AuthorizationHeader); value != "" {
		header.Set(name, value)
	}
}

func gitBlobSHA(data []byte) string {
	hash := sha1.New() // #nosec G401 -- verifies legacy Git object identity only.
	_, _ = io.WriteString(hash, "blob "+strconv.Itoa(len(data))+"\x00")
	_, _ = hash.Write(data)
	return hex.EncodeToString(hash.Sum(nil))
}

func rawJSONIdentity(raw json.RawMessage) (string, error) {
	if len(raw) == 0 {
		return "", fmt.Errorf("repository id is missing")
	}
	var text string
	if raw[0] == '"' {
		if err := json.Unmarshal(raw, &text); err != nil {
			return "", err
		}
		return strings.TrimSpace(text), nil
	}
	var number json.Number
	if err := json.Unmarshal(raw, &number); err != nil {
		return "", err
	}
	return strings.TrimSpace(number.String()), nil
}

func cleanKBSourceExportPath(raw string) (string, error) {
	if strings.Contains(raw, "\\") || strings.ContainsRune(raw, '\x00') {
		return "", fmt.Errorf("invalid source path")
	}
	clean := pathpkg.Clean(strings.TrimSpace(raw))
	if clean == "." || clean == "" || strings.HasPrefix(clean, "../") || strings.HasPrefix(clean, "/") || clean != strings.TrimSpace(raw) {
		return "", fmt.Errorf("invalid source path")
	}
	return clean, nil
}

func fullKBSourceCommitSHA(value string) bool {
	if len(value) != 40 {
		return false
	}
	_, err := hex.DecodeString(value)
	return err == nil
}

type boundedKBSourceMigrationResponse struct {
	header   http.Header
	status   int
	limit    int64
	body     strings.Builder
	exceeded bool
}

func newBoundedKBSourceMigrationResponse(limit int64) *boundedKBSourceMigrationResponse {
	return &boundedKBSourceMigrationResponse{header: make(http.Header), limit: limit}
}

func (response *boundedKBSourceMigrationResponse) Header() http.Header { return response.header }
func (response *boundedKBSourceMigrationResponse) WriteHeader(status int) {
	if response.status == 0 {
		response.status = status
	}
}

func (response *boundedKBSourceMigrationResponse) Write(data []byte) (int, error) {
	if response.status == 0 {
		response.status = http.StatusOK
	}
	remaining := response.limit + 1 - int64(response.body.Len())
	if remaining > 0 {
		keep := int64(len(data))
		if keep > remaining {
			keep = remaining
		}
		_, _ = response.body.Write(data[:keep])
	}
	if int64(response.body.Len()) > response.limit || int64(len(data)) > remaining {
		response.exceeded = true
	}
	return len(data), nil
}

func (response *boundedKBSourceMigrationResponse) result() ([]byte, error) {
	if response.exceeded {
		return nil, fmt.Errorf("AGS repository response exceeds byte limit")
	}
	status := response.status
	if status == 0 {
		status = http.StatusOK
	}
	if status < 200 || status >= 300 {
		return nil, fmt.Errorf("AGS repository response status %d", status)
	}
	return []byte(response.body.String()), nil
}
