package contracts

import (
	"fmt"
	"regexp"
	"strings"

	"github.com/zzet/gortex/internal/graph"
)

// OpenAPIExtractor detects OpenAPI/Swagger spec definitions (providers).
type OpenAPIExtractor struct{}

var (
	// Detect OpenAPI YAML structure: paths with HTTP methods.
	openapiPathRe   = regexp.MustCompile(`(?m)^  (\/\S+)\s*:`)
	openapiMethodRe = regexp.MustCompile(`(?m)^\s{4}(get|post|put|patch|delete|head|options)\s*:`)

	// Detect OpenAPI JSON structure.
	openapiJSONPathRe   = regexp.MustCompile(`"(\/[^"]+)"\s*:\s*\{`)
	openapiJSONMethodRe = regexp.MustCompile(`"(get|post|put|patch|delete|head|options)"\s*:\s*\{`)

	// Schema-shape extraction — these run over a single operation's
	// block to pull out request / response type names. Full OpenAPI
	// parsing is overkill for what downstream validation needs:
	// a reference to the `components.schemas.<Name>` block is enough.
	//
	// The optional `['"]?` after `\$ref` handles the JSON form where
	// the key is `"$ref"` with a trailing quote before the colon;
	// in YAML the key is a bare `$ref:` with nothing between the
	// identifier and the colon.
	openapiRefRe = regexp.MustCompile(`\$ref['"]?\s*:\s*['"]?#/components/schemas/(\w+)['"]?`)
	// YAML: `requestBody:` block anchor, then everything up to the
	// next sibling key or the end of the operation.
	openapiRequestBodyRe = regexp.MustCompile(`(?s)requestBody\s*:(.*?)(?:\n      \w|\z)`)
	// YAML: `responses:` block; within it, a status-code entry followed
	// by a `$ref`. We capture both the status and the ref.
	openapiResponseStatusRe = regexp.MustCompile(`(?m)^\s{8}['"]?(\d{3})['"]?\s*:`)
	// JSON status-code extraction. Brace-balanced slicing (see
	// jsonObjectSlice below) handles the request/response object
	// boundaries so no greedy regex fight with nested braces.
	openapiJSONStatusRe = regexp.MustCompile(`"(\d{3})"\s*:`)
)

func (e *OpenAPIExtractor) SupportedLanguages() []string {
	return []string{"yaml", "json"}
}

func (e *OpenAPIExtractor) Extract(filePath string, src []byte, nodes []*graph.Node, edges []*graph.Edge) []Contract {
	text := string(src)

	// Quick check: must contain either "openapi" or "swagger" key.
	lower := strings.ToLower(text)
	if !strings.Contains(lower, "openapi") && !strings.Contains(lower, "swagger") {
		return nil
	}

	if strings.HasSuffix(filePath, ".json") {
		return e.extractJSON(filePath, src)
	}
	return e.extractYAML(filePath, src)
}

func (e *OpenAPIExtractor) extractYAML(filePath string, src []byte) []Contract {
	var contracts []Contract
	text := string(src)
	lines := strings.Split(text, "\n")

	// Find the "paths:" section.
	pathsIdx := strings.Index(text, "\npaths:")
	if pathsIdx < 0 {
		if strings.HasPrefix(text, "paths:") {
			pathsIdx = 0
		} else {
			return nil
		}
	}
	pathsSection := text[pathsIdx:]

	// Find each path entry.
	pathMatches := openapiPathRe.FindAllStringSubmatchIndex(pathsSection, -1)
	for i, pm := range pathMatches {
		path := pathsSection[pm[2]:pm[3]]
		// Determine the sub-section for this path (up to next path or end).
		end := len(pathsSection)
		if i+1 < len(pathMatches) {
			end = pathMatches[i+1][0]
		}
		pathBlock := pathsSection[pm[0]:end]

		// Iterate method matches with explicit indices so we can
		// slice each operation's block from the method line to the
		// next sibling method / path — that's the window we scan
		// for requestBody / responses $refs.
		methodLocs := openapiMethodRe.FindAllStringSubmatchIndex(pathBlock, -1)
		methodMatches := openapiMethodRe.FindAllStringSubmatch(pathBlock, -1)
		for mi, mm := range methodMatches {
			method := strings.ToUpper(mm[1])
			absOffset := pathsIdx + pm[0]
			normPath := NormalizeHTTPPath(path)

			// Block for this operation: start at the method line,
			// end at the next operation or end-of-path.
			opStart := methodLocs[mi][0]
			opEnd := len(pathBlock)
			if mi+1 < len(methodLocs) {
				opEnd = methodLocs[mi+1][0]
			}
			opBlock := pathBlock[opStart:opEnd]

			meta := map[string]any{"method": method, "path": normPath}
			reqType, respType, statusCodes := openapiExtractSchemas(opBlock)
			if reqType != "" {
				meta["request_type"] = reqType
			}
			if respType != "" {
				meta["response_type"] = respType
			}
			if len(statusCodes) > 0 {
				meta["status_codes"] = statusCodes
			}
			if reqType != "" || respType != "" {
				meta["schema_source"] = "extracted"
			} else {
				meta["schema_source"] = "none"
			}

			contracts = append(contracts, Contract{
				ID:         fmt.Sprintf("http::%s::%s", method, normPath),
				Type:       ContractOpenAPI,
				Role:       RoleProvider,
				FilePath:   filePath,
				Line:       lineNumber(lines, absOffset),
				Meta:       meta,
				Confidence: 0.95,
			})
		}
	}

	return contracts
}

func (e *OpenAPIExtractor) extractJSON(filePath string, src []byte) []Contract {
	var contracts []Contract
	text := string(src)
	lines := strings.Split(text, "\n")

	pathMatches := openapiJSONPathRe.FindAllStringSubmatchIndex(text, -1)
	for i, pm := range pathMatches {
		path := text[pm[2]:pm[3]]
		if !strings.HasPrefix(path, "/") {
			continue
		}
		end := len(text)
		if i+1 < len(pathMatches) {
			end = pathMatches[i+1][0]
		}
		pathBlock := text[pm[0]:end]

		methodLocs := openapiJSONMethodRe.FindAllStringSubmatchIndex(pathBlock, -1)
		methodMatches := openapiJSONMethodRe.FindAllStringSubmatch(pathBlock, -1)
		for mi, mm := range methodMatches {
			method := strings.ToUpper(mm[1])
			normPath := NormalizeHTTPPath(path)
			opStart := methodLocs[mi][0]
			opEnd := len(pathBlock)
			if mi+1 < len(methodLocs) {
				opEnd = methodLocs[mi+1][0]
			}
			opBlock := pathBlock[opStart:opEnd]

			meta := map[string]any{"method": method, "path": normPath}
			reqType, respType, statusCodes := openapiExtractSchemasJSON(opBlock)
			if reqType != "" {
				meta["request_type"] = reqType
			}
			if respType != "" {
				meta["response_type"] = respType
			}
			if len(statusCodes) > 0 {
				meta["status_codes"] = statusCodes
			}
			if reqType != "" || respType != "" {
				meta["schema_source"] = "extracted"
			} else {
				meta["schema_source"] = "none"
			}
			contracts = append(contracts, Contract{
				ID:         fmt.Sprintf("http::%s::%s", method, normPath),
				Type:       ContractOpenAPI,
				Role:       RoleProvider,
				FilePath:   filePath,
				Line:       lineNumber(lines, pm[0]),
				Meta:       meta,
				Confidence: 0.9,
			})
		}
	}

	return contracts
}

// openapiExtractSchemas pulls the request / response body type names
// (from `$ref: '#/components/schemas/<X>'` entries) and the declared
// response status codes out of a single operation's YAML block.
// Returns bare schema names — the module-wide post-pass upgrades
// them to symbol IDs of the matching type nodes in the same spec
// file's components block.
func openapiExtractSchemas(opBlock string) (requestType, responseType string, statusCodes []int) {
	// Request body — scope the $ref search to the requestBody block
	// so a $ref under `parameters` or `responses` doesn't bleed in.
	if m := openapiRequestBodyRe.FindStringSubmatch(opBlock); len(m) > 1 {
		if r := openapiRefRe.FindStringSubmatch(m[1]); len(r) > 1 {
			requestType = r[1]
		}
	}
	// Responses — walk status entries in order, first $ref wins for
	// response_type (typically the 2xx body) and we record every
	// declared status code.
	respIdx := strings.Index(opBlock, "responses:")
	if respIdx >= 0 {
		respBlock := opBlock[respIdx:]
		for _, m := range openapiResponseStatusRe.FindAllStringSubmatch(respBlock, -1) {
			if n, ok := parseStatusExpr(m[1]); ok {
				statusCodes = append(statusCodes, n)
			}
		}
		if r := openapiRefRe.FindStringSubmatch(respBlock); len(r) > 1 {
			responseType = r[1]
		}
	}
	return
}

// openapiExtractSchemasJSON is the JSON counterpart. Regex-based
// capture of `"requestBody": { ... }` would misbehave on nested
// braces — instead we locate the key, brace-balance forward to find
// the matching close, and scan that slice for a $ref.
func openapiExtractSchemasJSON(opBlock string) (requestType, responseType string, statusCodes []int) {
	if slice := jsonObjectSlice(opBlock, `"requestBody"`); slice != "" {
		if r := openapiRefRe.FindStringSubmatch(slice); len(r) > 1 {
			requestType = r[1]
		}
	}
	if slice := jsonObjectSlice(opBlock, `"responses"`); slice != "" {
		for _, sm := range openapiJSONStatusRe.FindAllStringSubmatch(slice, -1) {
			if n, ok := parseStatusExpr(sm[1]); ok {
				statusCodes = append(statusCodes, n)
			}
		}
		if r := openapiRefRe.FindStringSubmatch(slice); len(r) > 1 {
			responseType = r[1]
		}
	}
	return
}

// jsonObjectSlice returns the textual body of the JSON object that
// follows `key` in src — i.e. the substring between the opening `{`
// and the matching `}`, with braces balanced. Returns empty string
// when the key isn't present or the object is malformed. Comments
// and strings aren't specially handled (JSON has no comments and
// `}` doesn't appear unescaped inside strings in any sane OpenAPI
// spec).
func jsonObjectSlice(src, key string) string {
	idx := strings.Index(src, key)
	if idx < 0 {
		return ""
	}
	// Skip to the first `{` after the key.
	open := strings.Index(src[idx:], "{")
	if open < 0 {
		return ""
	}
	open += idx
	depth := 0
	for i := open; i < len(src); i++ {
		switch src[i] {
		case '{':
			depth++
		case '}':
			depth--
			if depth == 0 {
				return src[open+1 : i]
			}
		}
	}
	return ""
}
