package kb

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path"
	"strings"
	"unicode/utf8"

	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

const maxKBRepositoryFileBytes = 2 * 1024 * 1024

type AGSHTTPKBWikiReader struct {
	Handler    http.Handler
	AuthHeader string
}

func (r AGSHTTPKBWikiReader) SearchWiki(ctx context.Context, request KBWikiSearchRequest) (KBWikiSearchResponse, error) {
	if r.Handler == nil {
		return KBWikiSearchResponse{}, invalidArgument("kb wiki reader handler is not configured")
	}
	if kbRepositoryFileRole(request.RepoRole) {
		return r.searchRepositoryFiles(ctx, request)
	}
	values := url.Values{}
	values.Set("q", strings.TrimSpace(request.Query))
	if request.Limit > 0 {
		values.Set("limit", fmt.Sprint(request.Limit))
	}
	if request.Offset > 0 {
		values.Set("offset", fmt.Sprint(request.Offset))
	}
	if revision := strings.TrimSpace(request.Revision); revision != "" {
		values.Set("ref", revision)
	}
	status, body := r.do(ctx, request.AuthorizationHeader, http.MethodGet, agsWikiPath(request.Owner, request.Repo, "search")+"?"+values.Encode(), nil)
	if status < 200 || status >= 300 {
		return KBWikiSearchResponse{}, kbWikiHTTPError(status, "search wiki", body)
	}
	var payload struct {
		Results []struct {
			Slug    string  `json:"slug"`
			Title   string  `json:"title"`
			Snippet string  `json:"snippet"`
			SHA     string  `json:"sha"`
			Score   float64 `json:"score"`
		} `json:"results"`
		Total   int  `json:"total"`
		HasMore bool `json:"has_more"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return KBWikiSearchResponse{}, fmt.Errorf("search wiki via AGS: decode response: %w", err)
	}
	out := KBWikiSearchResponse{Results: make([]KBWikiSearchHit, 0, len(payload.Results)), Total: payload.Total, HasMore: payload.HasMore}
	for _, hit := range payload.Results {
		out.Results = append(out.Results, KBWikiSearchHit{
			Slug:    strings.TrimSpace(hit.Slug),
			Title:   strings.TrimSpace(hit.Title),
			Snippet: strings.TrimSpace(hit.Snippet),
			SHA:     strings.TrimSpace(hit.SHA),
			Score:   hit.Score,
		})
	}
	if out.Total == 0 {
		out.Total = len(out.Results)
	}
	return out, nil
}

func (r AGSHTTPKBWikiReader) ReadWikiPage(ctx context.Context, request KBWikiReadRequest) (KBWikiPage, error) {
	if r.Handler == nil {
		return KBWikiPage{}, invalidArgument("kb wiki reader handler is not configured")
	}
	if kbRepositoryFileRole(request.RepoRole) {
		return r.readRepositoryFile(ctx, request)
	}
	apiPath := agsWikiPath(request.Owner, request.Repo, "pages/"+url.PathEscape(strings.TrimSpace(request.Slug)))
	if revision := strings.TrimSpace(request.Revision); revision != "" {
		values := url.Values{}
		values.Set("ref", revision)
		apiPath += "?" + values.Encode()
	}
	status, body := r.do(ctx, request.AuthorizationHeader, http.MethodGet, apiPath, nil)
	if status < 200 || status >= 300 {
		return KBWikiPage{}, kbWikiHTTPError(status, "read wiki page", body)
	}
	var payload struct {
		Slug  string `json:"slug"`
		Title string `json:"title"`
		Body  string `json:"body"`
		SHA   string `json:"sha"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return KBWikiPage{}, fmt.Errorf("read wiki page via AGS: decode response: %w", err)
	}
	return KBWikiPage{Slug: strings.TrimSpace(payload.Slug), Title: strings.TrimSpace(payload.Title), Body: payload.Body, SHA: strings.TrimSpace(payload.SHA)}, nil
}

func (r AGSHTTPKBWikiReader) ListWikiTree(ctx context.Context, request KBWikiTreeRequest) ([]KBWikiTreeEntry, error) {
	if r.Handler == nil {
		return nil, invalidArgument("kb wiki reader handler is not configured")
	}
	if kbRepositoryFileRole(request.RepoRole) {
		return r.listRepositoryTree(ctx, request)
	}
	values := url.Values{}
	if treePath := strings.Trim(strings.TrimSpace(request.Path), "/"); treePath != "" {
		values.Set("path", treePath)
	}
	if revision := strings.TrimSpace(request.Revision); revision != "" {
		values.Set("ref", revision)
	}
	apiPath := agsWikiPath(request.Owner, request.Repo, "tree")
	if encoded := values.Encode(); encoded != "" {
		apiPath += "?" + encoded
	}
	status, body := r.do(ctx, request.AuthorizationHeader, http.MethodGet, apiPath, nil)
	if status < 200 || status >= 300 {
		return nil, kbWikiHTTPError(status, "list wiki tree", body)
	}
	var payload []struct {
		Path  string `json:"path"`
		Name  string `json:"name"`
		Kind  string `json:"kind"`
		Slug  string `json:"slug"`
		Title string `json:"title"`
		SHA   string `json:"sha"`
		Size  int64  `json:"size"`
		IsDir bool   `json:"is_dir"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, fmt.Errorf("list wiki tree via AGS: decode response: %w", err)
	}
	out := make([]KBWikiTreeEntry, 0, len(payload))
	for _, entry := range payload {
		name := strings.TrimSpace(entry.Name)
		if name == "" {
			name = path.Base(strings.TrimSpace(entry.Path))
		}
		kind := strings.TrimSpace(entry.Kind)
		out = append(out, KBWikiTreeEntry{
			Path:  strings.TrimSpace(entry.Path),
			Name:  name,
			Kind:  kind,
			Slug:  strings.TrimSpace(entry.Slug),
			Title: strings.TrimSpace(entry.Title),
			IsDir: entry.IsDir || kind == "dir" || kind == "directory" || kind == "tree",
			SHA:   strings.TrimSpace(entry.SHA),
			Size:  entry.Size,
		})
	}
	return out, nil
}

func kbRepositoryFileRole(repoRole string) bool {
	switch strings.TrimSpace(repoRole) {
	case repos.RepoRoleMirror, repos.RepoRolePrimary:
		return true
	default:
		return false
	}
}

func (r AGSHTTPKBWikiReader) searchRepositoryFiles(ctx context.Context, request KBWikiSearchRequest) (KBWikiSearchResponse, error) {
	limit := request.Limit
	if limit <= 0 {
		limit = defaultKBSearchLimit
	}
	if limit > 100 {
		limit = 100
	}
	offset := request.Offset
	if offset < 0 {
		offset = 0
	}
	values := url.Values{}
	values.Set("q", scopedRepositoryCodeQuery(request.Query, request.Owner, request.Repo))
	values.Set("page", "1")
	// AGS code search returns one item per matching line, while Knowledge search
	// returns one hit per file. Fetch the largest page so line-level duplicates
	// do not starve the requested number of unique files.
	values.Set("per_page", "100")
	values.Set("text_matches", "1")
	status, body := r.do(ctx, request.AuthorizationHeader, http.MethodGet, "/api/v3/search/code?"+values.Encode(), nil)
	if status < 200 || status >= 300 {
		return KBWikiSearchResponse{}, kbWikiHTTPError(status, "search repository files", body)
	}
	var payload struct {
		Total int `json:"total_count"`
		Items []struct {
			Path       string  `json:"path"`
			Name       string  `json:"name"`
			Score      float64 `json:"score"`
			LineNumber int     `json:"line_number"`
			Repository struct {
				FullName string `json:"full_name"`
			} `json:"repository"`
			TextMatches []struct {
				Fragment string `json:"fragment"`
			} `json:"text_matches"`
		} `json:"items"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return KBWikiSearchResponse{}, fmt.Errorf("search repository files via AGS: decode response: %w", err)
	}
	results := make([]KBWikiSearchHit, 0, len(payload.Items))
	seen := make(map[string]int, len(payload.Items))
	expectedRepo := strings.TrimSpace(request.Owner) + "/" + strings.TrimSpace(request.Repo)
	for _, item := range payload.Items {
		if strings.TrimSpace(item.Repository.FullName) != expectedRepo {
			continue
		}
		filePath := repositoryCodeSearchPath(item.Path)
		if filePath == "" {
			continue
		}
		snippet := ""
		for _, match := range item.TextMatches {
			fragment := strings.TrimSpace(match.Fragment)
			if fragment == "" {
				continue
			}
			snippet = fragment
			break
		}
		if item.LineNumber > 0 && snippet != "" {
			snippet = fmt.Sprintf("Line %d: %s", item.LineNumber, snippet)
		}
		if index, ok := seen[filePath]; ok {
			if results[index].Snippet == "" && snippet != "" {
				results[index].Snippet = snippet
			}
			if item.Score > results[index].Score {
				results[index].Score = item.Score
			}
			continue
		}
		seen[filePath] = len(results)
		results = append(results, KBWikiSearchHit{
			Slug:    filePath,
			Title:   filePath,
			Snippet: snippet,
			SHA:     strings.TrimSpace(request.Revision),
			Score:   item.Score,
		})
	}
	if offset >= len(results) {
		return KBWikiSearchResponse{Results: []KBWikiSearchHit{}, Total: len(results), HasMore: payload.Total > len(payload.Items)}, nil
	}
	end := offset + limit
	if end > len(results) {
		end = len(results)
	}
	return KBWikiSearchResponse{
		Results: results[offset:end],
		Total:   len(results),
		HasMore: end < len(results) || payload.Total > len(payload.Items),
	}, nil
}

func repositoryCodeSearchPath(raw string) string {
	filePath := strings.Trim(strings.TrimSpace(raw), "/")
	if filePath == "" {
		return filePath
	}
	// Some AGS backends expose line matches as pseudo paths such as
	// "README.md:79" or "README.md:79:matching text". These are not readable
	// repository paths, so strip the first numeric line suffix and merge all
	// matches back into the underlying file hit.
	for colon := strings.Index(filePath, ":"); colon >= 0; {
		lineStart := colon + 1
		lineEnd := lineStart
		for lineEnd < len(filePath) && filePath[lineEnd] >= '0' && filePath[lineEnd] <= '9' {
			lineEnd++
		}
		if lineEnd > lineStart && (lineEnd == len(filePath) || filePath[lineEnd] == ':') {
			return filePath[:colon]
		}
		next := strings.Index(filePath[lineStart:], ":")
		if next < 0 {
			break
		}
		colon = lineStart + next
	}
	return filePath
}

func scopedRepositoryCodeQuery(query, owner, repo string) string {
	parts := strings.Fields(strings.TrimSpace(query))
	filtered := make([]string, 0, len(parts)+1)
	for _, part := range parts {
		trimmed := strings.Trim(strings.TrimSpace(part), "()")
		if strings.HasPrefix(strings.ToLower(trimmed), "repo:") {
			continue
		}
		filtered = append(filtered, part)
	}
	filtered = append(filtered, "repo:"+strings.TrimSpace(owner)+"/"+strings.TrimSpace(repo))
	return strings.TrimSpace(strings.Join(filtered, " "))
}

func (r AGSHTTPKBWikiReader) readRepositoryFile(ctx context.Context, request KBWikiReadRequest) (KBWikiPage, error) {
	filePath, err := normalizeRepositoryRelativePath(request.Slug, false)
	if err != nil {
		return KBWikiPage{}, err
	}
	values := url.Values{}
	if revision := strings.TrimSpace(request.Revision); revision != "" {
		values.Set("ref", revision)
	}
	apiPath := agsRepositoryContentsPath(request.Owner, request.Repo, filePath)
	if encoded := values.Encode(); encoded != "" {
		apiPath += "?" + encoded
	}
	status, body := r.do(ctx, request.AuthorizationHeader, http.MethodGet, apiPath, nil)
	if status < 200 || status >= 300 {
		return KBWikiPage{}, kbWikiHTTPError(status, "read repository file", body)
	}
	var payload struct {
		Type     string `json:"type"`
		Name     string `json:"name"`
		Path     string `json:"path"`
		Size     int64  `json:"size"`
		Encoding string `json:"encoding"`
		Content  string `json:"content"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return KBWikiPage{}, fmt.Errorf("read repository file via AGS: decode response: %w", err)
	}
	if strings.TrimSpace(payload.Type) != "file" {
		return KBWikiPage{}, codedNotFound("not_found", "repository file not found")
	}
	if payload.Size > maxKBRepositoryFileBytes {
		return KBWikiPage{}, invalidArgument(fmt.Sprintf("repository file exceeds %d bytes", maxKBRepositoryFileBytes))
	}
	content := []byte(payload.Content)
	if strings.EqualFold(strings.TrimSpace(payload.Encoding), "base64") {
		decoded, err := base64.StdEncoding.DecodeString(payload.Content)
		if err != nil {
			return KBWikiPage{}, fmt.Errorf("read repository file via AGS: decode base64 content: %w", err)
		}
		content = decoded
	}
	if len(content) > maxKBRepositoryFileBytes {
		return KBWikiPage{}, invalidArgument(fmt.Sprintf("repository file exceeds %d bytes", maxKBRepositoryFileBytes))
	}
	if !utf8.Valid(content) {
		return KBWikiPage{}, invalidArgument("repository file is not UTF-8 text")
	}
	resolvedPath := strings.Trim(strings.TrimSpace(payload.Path), "/")
	if resolvedPath == "" {
		resolvedPath = filePath
	}
	title := strings.TrimSpace(payload.Name)
	if title == "" {
		title = resolvedPath
	}
	return KBWikiPage{
		Slug:  resolvedPath,
		Title: title,
		Body:  string(content),
		SHA:   strings.TrimSpace(request.Revision),
	}, nil
}

func (r AGSHTTPKBWikiReader) listRepositoryTree(ctx context.Context, request KBWikiTreeRequest) ([]KBWikiTreeEntry, error) {
	treePath, err := normalizeRepositoryRelativePath(request.Path, true)
	if err != nil {
		return nil, err
	}
	values := url.Values{}
	if revision := strings.TrimSpace(request.Revision); revision != "" {
		values.Set("ref", revision)
	}
	apiPath := agsRepositoryContentsPath(request.Owner, request.Repo, treePath)
	if encoded := values.Encode(); encoded != "" {
		apiPath += "?" + encoded
	}
	status, body := r.do(ctx, request.AuthorizationHeader, http.MethodGet, apiPath, nil)
	if status < 200 || status >= 300 {
		return nil, kbWikiHTTPError(status, "list repository tree", body)
	}
	var payload []struct {
		Type string `json:"type"`
		Name string `json:"name"`
		Path string `json:"path"`
		Size int64  `json:"size"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, fmt.Errorf("list repository tree via AGS: decode response: %w", err)
	}
	out := make([]KBWikiTreeEntry, 0, len(payload))
	for _, entry := range payload {
		entryPath := strings.Trim(strings.TrimSpace(entry.Path), "/")
		if entryPath == "" {
			continue
		}
		isDir := strings.EqualFold(strings.TrimSpace(entry.Type), "dir")
		kind := "file"
		slug := entryPath
		if isDir {
			kind = "directory"
			slug = ""
		}
		name := strings.TrimSpace(entry.Name)
		if name == "" {
			name = path.Base(entryPath)
		}
		out = append(out, KBWikiTreeEntry{
			Path:  entryPath,
			Name:  name,
			Kind:  kind,
			Slug:  slug,
			Title: entryPath,
			IsDir: isDir,
			SHA:   strings.TrimSpace(request.Revision),
			Size:  entry.Size,
		})
	}
	return out, nil
}

func normalizeRepositoryRelativePath(raw string, allowEmpty bool) (string, error) {
	filePath := strings.Trim(strings.TrimSpace(raw), "/")
	if filePath == "" {
		if allowEmpty {
			return "", nil
		}
		return "", invalidArgument("repository file path is required")
	}
	if strings.ContainsRune(filePath, '\x00') || strings.Contains(filePath, "\\") {
		return "", invalidArgument("repository file path is invalid")
	}
	for _, segment := range strings.Split(filePath, "/") {
		if segment == "" || segment == "." || segment == ".." {
			return "", invalidArgument("repository file path is invalid")
		}
	}
	return filePath, nil
}

func agsRepositoryContentsPath(owner, repo, filePath string) string {
	base := "/api/v3/repos/" + url.PathEscape(strings.TrimSpace(owner)) + "/" + url.PathEscape(strings.TrimSpace(repo)) + "/contents"
	filePath = strings.Trim(strings.TrimSpace(filePath), "/")
	if filePath == "" {
		return base
	}
	segments := strings.Split(filePath, "/")
	for index := range segments {
		segments[index] = url.PathEscape(segments[index])
	}
	return base + "/" + strings.Join(segments, "/")
}

func (r AGSHTTPKBWikiReader) do(ctx context.Context, auth, method, apiPath string, body io.Reader) (int, []byte) {
	req := httptest.NewRequest(method, apiPath, body).WithContext(ctx)
	setAGSAuthHeader(req.Header, r.AuthHeader, auth)
	rec := httptest.NewRecorder()
	r.Handler.ServeHTTP(rec, req)
	return rec.Code, rec.Body.Bytes()
}

func agsWikiPath(owner, repo, suffix string) string {
	return "/api/v3/repos/" + url.PathEscape(strings.TrimSpace(owner)) + "/" + url.PathEscape(strings.TrimSpace(repo)) + "/wiki/" + strings.TrimPrefix(suffix, "/")
}

func kbWikiHTTPError(statusCode int, action string, body []byte) error {
	message := strings.TrimSpace(string(body))
	if message == "" {
		message = http.StatusText(statusCode)
	}
	switch statusCode {
	case http.StatusUnauthorized:
		return codedUnauthorized("permission_denied", "AGS wiki failed while trying to "+action+": "+message)
	case http.StatusForbidden:
		return codedForbidden("permission_denied", "AGS wiki failed while trying to "+action+": "+message)
	case http.StatusNotFound:
		return codedNotFound("not_found", "AGS wiki failed while trying to "+action+": "+message)
	case http.StatusBadRequest, http.StatusConflict, http.StatusUnprocessableEntity:
		return invalidArgument("AGS wiki failed while trying to " + action + ": " + message)
	default:
		return fmt.Errorf("AGS wiki failed while trying to %s: %s", action, message)
	}
}
