// Package cloudflare provides a Cloudflare DNS adapter implementing port.DNSProvider.
package cloudflare

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

// Ensure Client implements DNSProvider.
var _ port.DNSProvider = (*Client)(nil)

const apiBase = "https://api.cloudflare.com/client/v4"

// Client is a Cloudflare DNS API client adapter.
type Client struct {
	apiToken string
	zoneID   string
	zoneName string // e.g., "threesix.ai"
	http     *http.Client
}

// NewClient creates a new Cloudflare DNS client.
// apiToken is a Cloudflare API token with DNS edit permissions
// zoneID is the Cloudflare zone ID for the domain
// zoneName is the domain name (e.g., "threesix.ai")
func NewClient(apiToken, zoneID, zoneName string) *Client {
	return &Client{
		apiToken: apiToken,
		zoneID:   zoneID,
		zoneName: zoneName,
		http: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// CreateRecord creates a DNS record.
func (c *Client) CreateRecord(ctx context.Context, record domain.DNSRecord) (*domain.DNSRecord, error) {
	// Normalize name: if just subdomain, append zone name
	name := c.normalizeName(record.Name)

	body := map[string]interface{}{
		"type":    record.Type,
		"name":    name,
		"content": record.Content,
		"ttl":     record.TTL,
		"proxied": record.Proxied,
	}

	resp, err := c.doRequest(ctx, "POST", fmt.Sprintf("/zones/%s/dns_records", c.zoneID), body)
	if err != nil {
		return nil, fmt.Errorf("failed to create DNS record: %w", err)
	}

	var result cfResponse
	if err := json.Unmarshal(resp, &result); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if !result.Success {
		return nil, &CloudflareError{Errors: result.Errors}
	}

	return recordFromCF(result.Result), nil
}

// UpdateRecord updates an existing DNS record by ID.
func (c *Client) UpdateRecord(ctx context.Context, recordID string, record domain.DNSRecord) (*domain.DNSRecord, error) {
	name := c.normalizeName(record.Name)

	body := map[string]interface{}{
		"type":    record.Type,
		"name":    name,
		"content": record.Content,
		"ttl":     record.TTL,
		"proxied": record.Proxied,
	}

	resp, err := c.doRequest(ctx, "PUT", fmt.Sprintf("/zones/%s/dns_records/%s", c.zoneID, recordID), body)
	if err != nil {
		return nil, fmt.Errorf("failed to update DNS record: %w", err)
	}

	var result cfResponse
	if err := json.Unmarshal(resp, &result); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if !result.Success {
		return nil, &CloudflareError{Errors: result.Errors}
	}

	return recordFromCF(result.Result), nil
}

// DeleteRecord removes a DNS record by ID.
func (c *Client) DeleteRecord(ctx context.Context, recordID string) error {
	_, err := c.doRequest(ctx, "DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", c.zoneID, recordID), nil)
	if err != nil {
		return fmt.Errorf("failed to delete DNS record: %w", err)
	}
	return nil
}

// DeleteRecordByName removes a DNS record by type and name.
func (c *Client) DeleteRecordByName(ctx context.Context, recordType, name string) error {
	record, err := c.FindRecord(ctx, recordType, name)
	if err != nil {
		return err
	}
	if record == nil {
		return nil // Already doesn't exist
	}
	return c.DeleteRecord(ctx, record.ID)
}

// GetRecord returns a single record by ID.
func (c *Client) GetRecord(ctx context.Context, recordID string) (*domain.DNSRecord, error) {
	resp, err := c.doRequest(ctx, "GET", fmt.Sprintf("/zones/%s/dns_records/%s", c.zoneID, recordID), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get DNS record: %w", err)
	}

	var result cfResponse
	if err := json.Unmarshal(resp, &result); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if !result.Success {
		return nil, &CloudflareError{Errors: result.Errors}
	}

	return recordFromCF(result.Result), nil
}

// ListRecords returns all records in the zone.
func (c *Client) ListRecords(ctx context.Context, recordType string) ([]*domain.DNSRecord, error) {
	path := fmt.Sprintf("/zones/%s/dns_records?per_page=100", c.zoneID)
	if recordType != "" {
		path += "&type=" + recordType
	}

	resp, err := c.doRequest(ctx, "GET", path, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to list DNS records: %w", err)
	}

	var result cfListResponse
	if err := json.Unmarshal(resp, &result); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if !result.Success {
		return nil, &CloudflareError{Errors: result.Errors}
	}

	records := make([]*domain.DNSRecord, len(result.Result))
	for i, r := range result.Result {
		records[i] = recordFromCFMap(r)
	}
	return records, nil
}

// FindRecord finds a record by type and name.
func (c *Client) FindRecord(ctx context.Context, recordType, name string) (*domain.DNSRecord, error) {
	normalizedName := c.normalizeName(name)
	path := fmt.Sprintf("/zones/%s/dns_records?type=%s&name=%s", c.zoneID, recordType, normalizedName)

	resp, err := c.doRequest(ctx, "GET", path, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to find DNS record: %w", err)
	}

	var result cfListResponse
	if err := json.Unmarshal(resp, &result); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if !result.Success {
		return nil, &CloudflareError{Errors: result.Errors}
	}

	if len(result.Result) == 0 {
		return nil, nil
	}

	return recordFromCFMap(result.Result[0]), nil
}

// UpsertRecord creates or updates a DNS record.
// If a record with the same type and name exists, it updates it.
// Otherwise, it creates a new record.
// Returns the created or updated record.
// Handles race conditions with retry logic.
func (c *Client) UpsertRecord(ctx context.Context, record domain.DNSRecord) (*domain.DNSRecord, error) {
	const maxRetries = 3

	for attempt := 0; attempt < maxRetries; attempt++ {
		// Try to find existing record
		existing, err := c.FindRecord(ctx, record.Type, record.Name)
		if err != nil {
			return nil, fmt.Errorf("failed to check for existing record: %w", err)
		}

		if existing != nil {
			// Update existing record
			updated, err := c.UpdateRecord(ctx, existing.ID, record)
			if err != nil {
				return nil, fmt.Errorf("failed to update existing record: %w", err)
			}
			return updated, nil
		}

		// Create new record
		created, err := c.CreateRecord(ctx, record)
		if err == nil {
			return created, nil
		}

		// Handle race condition: record was created between Find and Create
		if !isRecordExistsError(err) {
			return nil, fmt.Errorf("failed to create record: %w", err)
		}

		// Race condition detected - retry the whole find-or-create loop
		// This handles the case where another process created the record
	}

	return nil, fmt.Errorf("failed to upsert record after %d attempts due to concurrent modifications", maxRetries)
}

// isRecordExistsError checks if the error indicates a duplicate record.
func isRecordExistsError(err error) bool {
	if err == nil {
		return false
	}
	// Cloudflare returns "A record with that host already exists" or similar
	errStr := err.Error()
	return strings.Contains(errStr, "already exists") || strings.Contains(errStr, "duplicate")
}

// normalizeName converts a subdomain to full domain name.
func (c *Client) normalizeName(name string) string {
	if name == "@" || name == "" {
		return c.zoneName
	}
	// If already has zone suffix, return as-is
	if len(name) > len(c.zoneName) && name[len(name)-len(c.zoneName):] == c.zoneName {
		return name
	}
	return name + "." + c.zoneName
}

// doRequest performs an HTTP request to the Cloudflare API.
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) {
	var bodyReader io.Reader
	if body != nil {
		jsonBody, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		bodyReader = bytes.NewReader(jsonBody)
	}

	req, err := http.NewRequestWithContext(ctx, method, apiBase+path, bodyReader)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+c.apiToken)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.http.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() { _ = resp.Body.Close() }()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
	}

	return respBody, nil
}

// Cloudflare API response types
type cfResponse struct {
	Success bool                   `json:"success"`
	Errors  []cfError              `json:"errors"`
	Result  map[string]interface{} `json:"result"`
}

type cfListResponse struct {
	Success bool                     `json:"success"`
	Errors  []cfError                `json:"errors"`
	Result  []map[string]interface{} `json:"result"`
}

type cfError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

// CloudflareError wraps Cloudflare API errors with structured data.
type CloudflareError struct {
	Errors []cfError
}

func (e *CloudflareError) Error() string {
	msgs := make([]string, len(e.Errors))
	for i, ce := range e.Errors {
		msgs[i] = fmt.Sprintf("[%d] %s", ce.Code, ce.Message)
	}
	return "cloudflare API: " + strings.Join(msgs, "; ")
}

// HasCode returns true if any of the errors has the given code.
func (e *CloudflareError) HasCode(code int) bool {
	for _, ce := range e.Errors {
		if ce.Code == code {
			return true
		}
	}
	return false
}

// recordFromCF converts a Cloudflare record response to domain.DNSRecord.
func recordFromCF(r map[string]interface{}) *domain.DNSRecord {
	return recordFromCFMap(r)
}

func recordFromCFMap(r map[string]interface{}) *domain.DNSRecord {
	record := &domain.DNSRecord{}

	if id, ok := r["id"].(string); ok {
		record.ID = id
	}
	if t, ok := r["type"].(string); ok {
		record.Type = t
	}
	if name, ok := r["name"].(string); ok {
		record.Name = name
	}
	if content, ok := r["content"].(string); ok {
		record.Content = content
	}
	if ttl, ok := r["ttl"].(float64); ok {
		record.TTL = int(ttl)
	}
	if proxied, ok := r["proxied"].(bool); ok {
		record.Proxied = proxied
	}

	return record
}
