package hosting

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

// Client is the Synor Hosting SDK client
type Client struct {
	config     Config
	httpClient *http.Client
	closed     bool
}

// New creates a new Hosting client
func New(config Config) *Client {
	if config.Endpoint == "" {
		config.Endpoint = "https://hosting.synor.io/v1"
	}
	if config.Timeout == 0 {
		config.Timeout = 60 * time.Second
	}
	if config.Retries == 0 {
		config.Retries = 3
	}

	return &Client{
		config: config,
		httpClient: &http.Client{
			Timeout: config.Timeout,
		},
	}
}

// ==================== Domain Operations ====================

// CheckAvailability checks domain availability
func (c *Client) CheckAvailability(ctx context.Context, name string) (*DomainAvailability, error) {
	var result DomainAvailability
	err := c.request(ctx, "GET", "/domains/check/"+url.PathEscape(name), nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// RegisterDomain registers a new domain
func (c *Client) RegisterDomain(ctx context.Context, name string, opts *RegisterDomainOptions) (*Domain, error) {
	body := map[string]interface{}{
		"name": name,
	}
	if opts != nil {
		if opts.Years > 0 {
			body["years"] = opts.Years
		}
		body["auto_renew"] = opts.AutoRenew
		if opts.Records != nil {
			body["records"] = opts.Records
		}
	}

	var result Domain
	err := c.request(ctx, "POST", "/domains", body, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// GetDomain gets domain information
func (c *Client) GetDomain(ctx context.Context, name string) (*Domain, error) {
	var result Domain
	err := c.request(ctx, "GET", "/domains/"+url.PathEscape(name), nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// ListDomains lists all domains
func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {
	var result struct {
		Domains []Domain `json:"domains"`
	}
	err := c.request(ctx, "GET", "/domains", nil, &result)
	if err != nil {
		return nil, err
	}
	return result.Domains, nil
}

// UpdateDomainRecord updates domain record
func (c *Client) UpdateDomainRecord(ctx context.Context, name string, record *DomainRecord) (*Domain, error) {
	var result Domain
	err := c.request(ctx, "PUT", "/domains/"+url.PathEscape(name)+"/record", record, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// ResolveDomain resolves a domain
func (c *Client) ResolveDomain(ctx context.Context, name string) (*DomainRecord, error) {
	var result DomainRecord
	err := c.request(ctx, "GET", "/domains/"+url.PathEscape(name)+"/resolve", nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// RenewDomain renews a domain
func (c *Client) RenewDomain(ctx context.Context, name string, years int) (*Domain, error) {
	var result Domain
	err := c.request(ctx, "POST", "/domains/"+url.PathEscape(name)+"/renew", map[string]interface{}{"years": years}, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// ==================== DNS Operations ====================

// GetDnsZone gets DNS zone for a domain
func (c *Client) GetDnsZone(ctx context.Context, domain string) (*DnsZone, error) {
	var result DnsZone
	err := c.request(ctx, "GET", "/dns/"+url.PathEscape(domain), nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// SetDnsRecords sets DNS records for a domain
func (c *Client) SetDnsRecords(ctx context.Context, domain string, records []DnsRecord) (*DnsZone, error) {
	var result DnsZone
	err := c.request(ctx, "PUT", "/dns/"+url.PathEscape(domain), map[string]interface{}{"records": records}, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// AddDnsRecord adds a DNS record
func (c *Client) AddDnsRecord(ctx context.Context, domain string, record DnsRecord) (*DnsZone, error) {
	var result DnsZone
	err := c.request(ctx, "POST", "/dns/"+url.PathEscape(domain)+"/records", record, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// DeleteDnsRecord deletes a DNS record
func (c *Client) DeleteDnsRecord(ctx context.Context, domain, recordType, name string) (*DnsZone, error) {
	var result DnsZone
	path := fmt.Sprintf("/dns/%s/records/%s/%s", url.PathEscape(domain), recordType, url.PathEscape(name))
	err := c.request(ctx, "DELETE", path, nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// ==================== Deployment Operations ====================

// Deploy deploys a site from CID
func (c *Client) Deploy(ctx context.Context, cid string, opts *DeployOptions) (*Deployment, error) {
	body := map[string]interface{}{
		"cid": cid,
	}
	if opts != nil {
		if opts.Domain != "" {
			body["domain"] = opts.Domain
		}
		if opts.Subdomain != "" {
			body["subdomain"] = opts.Subdomain
		}
		if opts.Headers != nil {
			body["headers"] = opts.Headers
		}
		if opts.Redirects != nil {
			body["redirects"] = opts.Redirects
		}
		body["spa"] = opts.SPA
		body["clean_urls"] = opts.CleanURLs
		body["trailing_slash"] = opts.TrailingSlash
	}

	var result Deployment
	err := c.request(ctx, "POST", "/deployments", body, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// GetDeployment gets deployment by ID
func (c *Client) GetDeployment(ctx context.Context, id string) (*Deployment, error) {
	var result Deployment
	err := c.request(ctx, "GET", "/deployments/"+url.PathEscape(id), nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// ListDeployments lists deployments
func (c *Client) ListDeployments(ctx context.Context, domain string) ([]Deployment, error) {
	path := "/deployments"
	if domain != "" {
		path += "?domain=" + url.QueryEscape(domain)
	}

	var result struct {
		Deployments []Deployment `json:"deployments"`
	}
	err := c.request(ctx, "GET", path, nil, &result)
	if err != nil {
		return nil, err
	}
	return result.Deployments, nil
}

// Rollback rolls back to a previous deployment
func (c *Client) Rollback(ctx context.Context, domain, deploymentID string) (*Deployment, error) {
	var result Deployment
	err := c.request(ctx, "POST", "/deployments/"+url.PathEscape(deploymentID)+"/rollback", map[string]interface{}{"domain": domain}, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// DeleteDeployment deletes a deployment
func (c *Client) DeleteDeployment(ctx context.Context, id string) error {
	return c.request(ctx, "DELETE", "/deployments/"+url.PathEscape(id), nil, nil)
}

// GetDeploymentStats gets deployment stats
func (c *Client) GetDeploymentStats(ctx context.Context, id, period string) (*DeploymentStats, error) {
	path := fmt.Sprintf("/deployments/%s/stats?period=%s", url.PathEscape(id), url.QueryEscape(period))
	var result DeploymentStats
	err := c.request(ctx, "GET", path, nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// ==================== SSL Operations ====================

// ProvisionSSL provisions SSL certificate
func (c *Client) ProvisionSSL(ctx context.Context, domain string, opts *ProvisionSSLOptions) (*Certificate, error) {
	body := make(map[string]interface{})
	if opts != nil {
		body["include_www"] = opts.IncludeWWW
		body["auto_renew"] = opts.AutoRenew
	}

	var result Certificate
	err := c.request(ctx, "POST", "/ssl/"+url.PathEscape(domain), body, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// GetCertificate gets certificate status
func (c *Client) GetCertificate(ctx context.Context, domain string) (*Certificate, error) {
	var result Certificate
	err := c.request(ctx, "GET", "/ssl/"+url.PathEscape(domain), nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// RenewCertificate renews SSL certificate
func (c *Client) RenewCertificate(ctx context.Context, domain string) (*Certificate, error) {
	var result Certificate
	err := c.request(ctx, "POST", "/ssl/"+url.PathEscape(domain)+"/renew", nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// DeleteCertificate deletes/revokes SSL certificate
func (c *Client) DeleteCertificate(ctx context.Context, domain string) error {
	return c.request(ctx, "DELETE", "/ssl/"+url.PathEscape(domain), nil, nil)
}

// ==================== Site Configuration ====================

// GetSiteConfig gets site configuration
func (c *Client) GetSiteConfig(ctx context.Context, domain string) (*SiteConfig, error) {
	var result SiteConfig
	err := c.request(ctx, "GET", "/sites/"+url.PathEscape(domain)+"/config", nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// UpdateSiteConfig updates site configuration
func (c *Client) UpdateSiteConfig(ctx context.Context, domain string, config map[string]interface{}) (*SiteConfig, error) {
	var result SiteConfig
	err := c.request(ctx, "PATCH", "/sites/"+url.PathEscape(domain)+"/config", config, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// PurgeCache purges CDN cache
func (c *Client) PurgeCache(ctx context.Context, domain string, paths []string) (int, error) {
	body := make(map[string]interface{})
	if paths != nil {
		body["paths"] = paths
	}

	var result struct {
		Purged int `json:"purged"`
	}
	err := c.request(ctx, "DELETE", "/sites/"+url.PathEscape(domain)+"/cache", body, &result)
	if err != nil {
		return 0, err
	}
	return result.Purged, nil
}

// ==================== Analytics ====================

// GetAnalytics gets site analytics
func (c *Client) GetAnalytics(ctx context.Context, domain string, opts *AnalyticsOptions) (*AnalyticsData, error) {
	params := url.Values{}
	if opts != nil {
		if opts.Period != "" {
			params.Set("period", opts.Period)
		}
		if opts.StartDate != "" {
			params.Set("start", opts.StartDate)
		}
		if opts.EndDate != "" {
			params.Set("end", opts.EndDate)
		}
	}

	path := "/sites/" + url.PathEscape(domain) + "/analytics"
	if len(params) > 0 {
		path += "?" + params.Encode()
	}

	var result AnalyticsData
	err := c.request(ctx, "GET", path, nil, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// ==================== Lifecycle ====================

// Close closes the client
func (c *Client) Close() {
	c.closed = true
}

// IsClosed returns whether the client is closed
func (c *Client) IsClosed() bool {
	return c.closed
}

// HealthCheck performs a health check
func (c *Client) HealthCheck(ctx context.Context) bool {
	var result struct {
		Status string `json:"status"`
	}
	err := c.request(ctx, "GET", "/health", nil, &result)
	return err == nil && result.Status == "healthy"
}

func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
	if c.closed {
		return NewHostingError("Client has been closed", "CLIENT_CLOSED", 0)
	}

	var lastErr error

	for attempt := 0; attempt < c.config.Retries; attempt++ {
		err := c.doRequest(ctx, method, path, body, result)
		if err == nil {
			return nil
		}

		lastErr = err
		if c.config.Debug {
			fmt.Printf("Attempt %d failed: %v\n", attempt+1, err)
		}

		if attempt < c.config.Retries-1 {
			time.Sleep(time.Duration(attempt+1) * time.Second)
		}
	}

	return lastErr
}

func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error {
	var bodyReader io.Reader
	if body != nil {
		jsonBody, err := json.Marshal(body)
		if err != nil {
			return err
		}
		bodyReader = bytes.NewReader(jsonBody)
	}

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

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-SDK-Version", "go/0.1.0")

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

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

	if resp.StatusCode >= 400 {
		var errResp struct {
			Message string `json:"message"`
			Error   string `json:"error"`
			Code    string `json:"code"`
		}
		json.Unmarshal(respBody, &errResp)

		msg := errResp.Message
		if msg == "" {
			msg = errResp.Error
		}
		if msg == "" {
			msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
		}

		return NewHostingError(msg, errResp.Code, resp.StatusCode)
	}

	if result != nil && len(respBody) > 0 {
		return json.Unmarshal(respBody, result)
	}

	return nil
}
