package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	appErrors "github.com/nexuscrm/backend/pkg/errors"
)

// Manifest matches the sales_crm/manifest.json structure
type Manifest struct {
	Objects    []ManifestObject    `json:"Objects"`
	Apps       []ManifestApp       `json:"Apps"`
	Dashboards []ManifestDashboard `json:"Dashboards"`
}

// ManifestObject is the object definition in manifest format
type ManifestObject struct {
	Name         string          `json:"name"`
	Label        string          `json:"label"`
	LabelPlural  string          `json:"labelPlural"`
	Icon         string          `json:"icon"`
	Description  string          `json:"description"`
	SharingModel string          `json:"sharingModel"`
	Searchable   bool            `json:"searchable"`
	ListFields   []string        `json:"listFields"`
	KanbanGroup  string          `json:"kanbanGroupBy"`
	Fields       []ManifestField `json:"fields"`
}

type ManifestField struct {
	Name        string   `json:"name"`
	Label       string   `json:"label"`
	Type        string   `json:"type"`
	Required    bool     `json:"required"`
	NameField   bool     `json:"nameField"`
	Options     []string `json:"options"`
	ReferenceTo []string `json:"referenceTo"`
	Scale       int      `json:"scale"`
	Precision   int      `json:"precision"`
}

// ManifestApp is the app definition in UI API format (from manifest)
type ManifestApp struct {
	AppID       string            `json:"appId"`
	Label       string            `json:"label"`
	Description string            `json:"description"`
	IconURL     string            `json:"iconUrl"`
	HeaderColor string            `json:"headerColor"`
	Selected    bool              `json:"selected"`
	NavItems    []ManifestNavItem `json:"navItems"`
}

type ManifestNavItem struct {
	DeveloperName string                 `json:"developerName"`
	ItemType      string                 `json:"itemType"`
	ObjectAPIName string                 `json:"objectApiName"`
	Label         string                 `json:"label"`
	IconURL       string                 `json:"iconUrl"`
	StandardType  string                 `json:"standardType"`
	PageReference *ManifestPageReference `json:"pageReference"`
}

type ManifestPageReference struct {
	Type       string            `json:"type"`
	Attributes map[string]string `json:"attributes"`
}

// ManifestDashboardComponent is a component entry in a manifest dashboard
type ManifestDashboardComponent struct {
	ReportID string                 `json:"reportId"`
	Type     string                 `json:"type"`
	Title    string                 `json:"title"`
	Config   map[string]interface{} `json:"config,omitempty"`
}

// ManifestDashboard is the dashboard definition in manifest format
type ManifestDashboard struct {
	Label         string                       `json:"label"`
	DeveloperName string                       `json:"developerName"`
	Description   string                       `json:"description"`
	Components    []ManifestDashboardComponent `json:"components,omitempty"`
}

var (
	baseURL string
	token   string
)

func main() {
	manifestPath := flag.String("manifest", "cmd/installer/apps/sales_crm/manifest.json", "Path to manifest.json")
	apiURL := flag.String("api", "http://127.0.0.1:3001/services/data/v63.0", "API Base URL")
	adminEmail := flag.String("email", "admin@test.com", "Admin Email")
	adminPass := flag.String("password", "", "Admin Password (required)")
	flag.Parse()

	if *adminPass == "" {
		log.Fatal("--password flag is required")
	}

	baseURL = *apiURL

	log.Println("Starting NexusCRM Installer...")
	log.Printf("   API: %s", baseURL)
	log.Printf("   Manifest: %s", *manifestPath)

	// 1. Authenticate
	if err := login(*adminEmail, *adminPass); err != nil {
		log.Fatalf("Login failed: %v", err)
	}
	log.Println("Login successful")

	// 2. Read Manifest
	manifestData, err := os.ReadFile(*manifestPath)
	if err != nil {
		log.Fatalf("Failed to read manifest: %v", err)
	}

	var manifest Manifest
	if err := json.Unmarshal(manifestData, &manifest); err != nil {
		log.Fatalf("Failed to parse manifest: %v", err)
	}
	log.Printf("Loaded %d objects, %d apps, %d dashboards", len(manifest.Objects), len(manifest.Apps), len(manifest.Dashboards))

	// 3. Install Objects
	for _, obj := range manifest.Objects {
		log.Printf("Installing object: %s", obj.Label)
		if objectExists(obj.Name) {
			log.Printf("   Already exists, skipping")
			continue
		}
		if err := createObject(obj); err != nil {
			log.Printf("   Failed: %v", err)
		} else {
			log.Printf("   Created %s", obj.Label)
		}
	}

	// 4. Install Apps
	// Two-step approach: Tooling API create (Salesforce spec) + SObject PATCH (full nav items)
	for _, app := range manifest.Apps {
		log.Printf("Installing app: %s", app.Label)
		if err := createApp(app); err != nil {
			log.Printf("   Failed: %v", err)
		} else {
			log.Printf("   Created %s", app.Label)
		}
	}

	// 5. Install Dashboards (need a folder first)
	if len(manifest.Dashboards) > 0 {
		folderID := getOrCreateDefaultFolder()
		if folderID == "" {
			log.Println("   Cannot create dashboards: no folder available")
		} else {
			for _, dash := range manifest.Dashboards {
				log.Printf("Installing dashboard: %s", dash.Label)
				if err := createDashboard(dash, folderID); err != nil {
					log.Printf("   Failed: %v", err)
				} else {
					log.Printf("   Created %s", dash.Label)
				}
			}
		}
	}

	log.Println("Installation Complete!")
}

func login(email, password string) error {
	reqBody := fmt.Sprintf("grant_type=password&username=%s&password=%s", email, password)

	// Derive auth URL from baseURL (strip /services/data/v63.0)
	authURL := strings.TrimSuffix(baseURL, "/services/data/v63.0")
	if authURL == baseURL {
		authURL = "http://127.0.0.1:3001"
	}

	resp, err := http.Post(authURL+"/services/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBufferString(reqBody))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		body, _ := io.ReadAll(resp.Body)
		return appErrors.NewInternalError(fmt.Sprintf("login failed with status %d: %s", resp.StatusCode, string(body)), nil)
	}

	var loginResp struct {
		AccessToken string `json:"access_token"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
		return err
	}

	token = loginResp.AccessToken
	return nil
}

func objectExists(name string) bool {
	req, _ := http.NewRequest("GET", baseURL+"/sobjects/"+name+"/describe", nil)
	req.Header.Set("Authorization", "Bearer "+token)

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

	return resp.StatusCode == 200
}

func createObject(obj ManifestObject) error {
	// CreateSchema expects camelCase: { name, label, labelPlural, description, icon }
	payload := map[string]interface{}{
		"name":        obj.Name,
		"label":       obj.Label,
		"labelPlural": obj.LabelPlural,
		"description": obj.Description,
		"icon":        obj.Icon,
		"custom":      true,
	}

	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", baseURL+"/tooling/sobjects/CustomObject", bytes.NewBuffer(body))
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")

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

	if resp.StatusCode >= 400 {
		respBody, _ := io.ReadAll(resp.Body)
		return appErrors.NewInternalError(fmt.Sprintf("create object failed with status %d: %s", resp.StatusCode, string(respBody)), nil)
	}

	return nil
}

// appExists checks if an app exists by developer name and returns its DB ID.
func appExists(appID string) (string, bool) {
	query := fmt.Sprintf("SELECT+Id+FROM+_System_App+WHERE+Name='%s'+LIMIT+1", appID)
	req, _ := http.NewRequest("GET", baseURL+"/query?q="+query, nil)
	req.Header.Set("Authorization", "Bearer "+token)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", false
	}
	defer resp.Body.Close()

	if resp.StatusCode == 200 {
		var result struct {
			TotalSize int `json:"totalSize"`
			Records   []struct {
				ID string `json:"Id"`
			} `json:"records"`
		}
		if err := json.NewDecoder(resp.Body).Decode(&result); err == nil && result.TotalSize > 0 {
			return result.Records[0].ID, true
		}
	}
	return "", false
}

// createApp installs an app using a two-step approach for Salesforce spec compliance:
//  1. Create via Tooling API (spec-compliant tabs, auto-creates AppMenuItem)
//  2. PATCH NavigationItems via SObject API (full-fidelity: labels, icons, dashboard refs)
//
// The Tooling API only accepts tab strings (e.g. "standard-Account"), which is lossy for
// non-object items (pages, dashboards). The SObject PATCH fixes this by writing the full
// NavigationItems JSON to the _System_App table.
func createApp(app ManifestApp) error {
	existingID, exists := appExists(app.AppID)
	if exists {
		log.Printf("   Already exists, patching navigation items")
		return patchAppNavItems(existingID, app)
	}

	// Step 1: Create via Tooling API with basic tab strings
	tabs := make([]string, 0, len(app.NavItems))
	for _, nav := range app.NavItems {
		switch nav.ItemType {
		case "object":
			tabs = append(tabs, "standard-"+nav.ObjectAPIName)
		default:
			tabs = append(tabs, nav.DeveloperName)
		}
	}

	payload := map[string]interface{}{
		"FullName": app.AppID,
		"Metadata": map[string]interface{}{
			"label":       app.Label,
			"description": app.Description,
			"tabs":        tabs,
		},
	}

	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", baseURL+"/tooling/sobjects/CustomApplication", bytes.NewBuffer(body))
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")

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

	if resp.StatusCode >= 400 {
		respBody, _ := io.ReadAll(resp.Body)
		return appErrors.NewInternalError(fmt.Sprintf("tooling API status %d: %s", resp.StatusCode, string(respBody)), nil)
	}

	// Step 2: Fetch the created app's DB ID and PATCH its NavigationItems
	createdID, found := appExists(app.AppID)
	if !found {
		return appErrors.NewInternalError(fmt.Sprintf("app '%s' was created but could not be found for nav item patching", app.AppID), nil)
	}

	return patchAppNavItems(createdID, app)
}

// patchAppNavItems updates the NavigationItems column via SObject PATCH API.
// The Tooling API's tab-string format is lossy for non-object nav types
// (pages, dashboards lose their labels, icons). This function writes the full
// NavigationItems JSON array directly to the _System_App table.
func patchAppNavItems(appID string, app ManifestApp) error {
	type navItem struct {
		ID            string `json:"id"`
		Type          string `json:"type"`
		ObjectAPIName string `json:"objectApiName,omitempty"`
		DashboardID   string `json:"dashboardId,omitempty"`
		Label         string `json:"label"`
		Icon          string `json:"icon,omitempty"`
	}

	items := make([]navItem, 0, len(app.NavItems))
	for _, nav := range app.NavItems {
		ni := navItem{
			ID:    nav.DeveloperName,
			Type:  nav.ItemType,
			Label: nav.Label,
			Icon:  nav.IconURL,
		}
		switch nav.ItemType {
		case "object":
			ni.ObjectAPIName = nav.ObjectAPIName
		case "dashboard":
			if nav.PageReference != nil {
				ni.DashboardID = nav.PageReference.Attributes["dashboardId"]
			}
		}
		items = append(items, ni)
	}

	// PATCH NavigationItems via SObject REST API
	// Include all required fields — the old record may have empty Color/Icon
	// from the initial Tooling API creation which doesn't set them
	navItemsJSON, _ := json.Marshal(items)
	color := app.HeaderColor
	if color == "" {
		color = "#1B96FF" // Salesforce default blue
	}
	icon := app.IconURL
	if icon == "" {
		icon = "standard:app"
	}
	patchPayload := map[string]interface{}{
		"Name":            app.AppID,
		"Label":           app.Label,
		"Description":     app.Description,
		"Color":           color,
		"Icon":            icon,
		"NavigationItems": json.RawMessage(navItemsJSON),
	}

	patchBody, _ := json.Marshal(patchPayload)
	patchReq, _ := http.NewRequest("PATCH", baseURL+"/sobjects/_System_App/"+appID, bytes.NewBuffer(patchBody))
	patchReq.Header.Set("Authorization", "Bearer "+token)
	patchReq.Header.Set("Content-Type", "application/json")

	patchResp, err := http.DefaultClient.Do(patchReq)
	if err != nil {
		return appErrors.NewInternalError("failed to patch nav items", err)
	}
	defer patchResp.Body.Close()

	if patchResp.StatusCode >= 400 {
		respBody, _ := io.ReadAll(patchResp.Body)
		return appErrors.NewInternalError(fmt.Sprintf("nav items PATCH status %d: %s", patchResp.StatusCode, string(respBody)), nil)
	}

	return nil
}

func getOrCreateDefaultFolder() string {
	// Query for existing folders via SObject API
	req, _ := http.NewRequest("GET", baseURL+"/query?q=SELECT+Id,Name+FROM+ReportFolder+LIMIT+1", nil)
	req.Header.Set("Authorization", "Bearer "+token)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Printf("   Failed to query folders: %v", err)
		return ""
	}
	defer resp.Body.Close()

	if resp.StatusCode == 200 {
		var result struct {
			Records []struct {
				ID string `json:"Id"`
			} `json:"records"`
		}
		if err := json.NewDecoder(resp.Body).Decode(&result); err == nil && len(result.Records) > 0 {
			log.Printf("   Using existing folder: %s", result.Records[0].ID)
			return result.Records[0].ID
		}
	}

	// Create a default folder via SObject API
	log.Println("   No folders found, creating default folder...")
	folderPayload := map[string]interface{}{
		"Name":          "Public Dashboards",
		"DeveloperName": "public_dashboards",
		"Type":          "Dashboard",
	}
	body, _ := json.Marshal(folderPayload)
	createReq, _ := http.NewRequest("POST", baseURL+"/sobjects/ReportFolder", bytes.NewBuffer(body))
	createReq.Header.Set("Authorization", "Bearer "+token)
	createReq.Header.Set("Content-Type", "application/json")

	createResp, err := http.DefaultClient.Do(createReq)
	if err != nil {
		log.Printf("   Failed to create folder: %v", err)
		return ""
	}
	defer createResp.Body.Close()

	if createResp.StatusCode >= 400 {
		respBody, _ := io.ReadAll(createResp.Body)
		log.Printf("   Failed to create folder: status %d: %s", createResp.StatusCode, string(respBody))
		return ""
	}

	var createResult struct {
		ID string `json:"id"`
	}
	if err := json.NewDecoder(createResp.Body).Decode(&createResult); err != nil {
		log.Printf("   Failed to parse folder response: %v", err)
		return ""
	}
	log.Printf("   Created default folder: %s", createResult.ID)
	return createResult.ID
}

func createDashboard(dash ManifestDashboard, folderID string) error {
	// CreateDashboard expects: { "name": "...", "folderId": "...", "description": "...", "components": [...] }
	payload := map[string]interface{}{
		"name":          dash.Label,
		"developerName": dash.DeveloperName,
		"folderId":      folderID,
		"description":   dash.Description,
		"components":    dash.Components,
	}

	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", baseURL+"/analytics/dashboards", bytes.NewBuffer(body))
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")

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

	if resp.StatusCode >= 400 {
		respBody, _ := io.ReadAll(resp.Body)
		bodyStr := string(respBody)
		if strings.Contains(bodyStr, "already exists") || strings.Contains(bodyStr, "DUPLICATE_VALUE") {
			log.Printf("   Already exists, skipping")
			return nil
		}
		return appErrors.NewInternalError(fmt.Sprintf("create dashboard failed with status %d: %s", resp.StatusCode, bodyStr), nil)
	}

	return nil
}
