package service

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"time"

	"security-center-backend/internal/modules/department/model"
	"security-center-backend/internal/modules/department/repository"
	"security-center-backend/internal/pkg/lark"
	"security-center-backend/internal/pkg/logger"
)

type DepartmentService struct {
	deptRepo    repository.DepartmentRepository
	empRepo     repository.EmployeeRepository
	runRepo     repository.OrgSyncRunRepository
	logger      *logger.Logger
	larkClient  *lark.Client
	syncMu      sync.Mutex
	syncRunning bool
}

const (
	stepDepartmentsFetch  = "departments_fetch"
	stepDepartmentsUpsert = "departments_upsert"
	stepEmployeesFetch    = "employees_fetch"
	stepEmployeesUpsert   = "employees_upsert"
)

func NewDepartmentService(
	deptRepo repository.DepartmentRepository,
	empRepo repository.EmployeeRepository,
	runRepo repository.OrgSyncRunRepository,
	logger *logger.Logger,
) *DepartmentService {
	return &DepartmentService{
		deptRepo: deptRepo,
		empRepo:  empRepo,
		runRepo:  runRepo,
		logger:   logger,
	}
}

func (s *DepartmentService) SetLarkClient(client *lark.Client) {
	s.larkClient = client
}

func (s *DepartmentService) LarkConfigured() bool {
	return s.larkClient != nil
}

func (s *DepartmentService) SyncFromLark(ctx context.Context) (*model.OrgSyncRun, error) {
	return s.syncFromLark(ctx, "manual")
}

func (s *DepartmentService) SyncFromLarkScheduled(ctx context.Context) (*model.OrgSyncRun, error) {
	return s.syncFromLark(ctx, "scheduled")
}

func (s *DepartmentService) StartSyncFromLarkAsync() (*model.OrgSyncRun, error) {
	if s.larkClient == nil {
		return nil, fmt.Errorf("lark client not configured")
	}

	if !s.tryStartSync() {
		return nil, fmt.Errorf("org sync already running")
	}

	run := s.newRun("manual")
	if s.runRepo != nil {
		_ = s.runRepo.Create(context.Background(), run)
	}

	go func(runID uint64) {
		defer s.finishSync()
		defer func() {
			if recovered := recover(); recovered != nil {
				err := fmt.Errorf("org sync panic: %v", recovered)
				_, _ = s.finalizeRun(context.Background(), run, 0, 0, 0, 0, 1, err.Error(), err)
			}
		}()

		bgCtx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
		defer cancel()
		if _, err := s.syncFromLarkWithRunNoLock(bgCtx, run); err != nil {
			s.logger.Error("async lark sync failed", "error", err, "run_id", runID)
		}
	}(run.ID)

	return run, nil
}

func (s *DepartmentService) syncFromLark(ctx context.Context, triggerSource string) (*model.OrgSyncRun, error) {
	run := s.newRun(triggerSource)
	if s.runRepo != nil {
		_ = s.runRepo.Create(ctx, run)
	}
	return s.syncFromLarkWithRun(ctx, run)
}

func (s *DepartmentService) newRun(triggerSource string) *model.OrgSyncRun {
	return &model.OrgSyncRun{
		Provider:      "lark",
		TriggerSource: triggerSource,
		Status:        "running",
		StartedAt:     time.Now().UTC(),
	}
}

func (s *DepartmentService) syncFromLarkWithRun(ctx context.Context, run *model.OrgSyncRun) (*model.OrgSyncRun, error) {
	if run == nil {
		return nil, fmt.Errorf("sync run is required")
	}
	if !s.tryStartSync() {
		err := fmt.Errorf("org sync already running")
		return s.finalizeRun(ctx, run, 0, 0, 0, 0, 1, err.Error(), err)
	}
	defer s.finishSync()
	return s.syncFromLarkWithRunNoLock(ctx, run)
}

func (s *DepartmentService) syncFromLarkWithRunNoLock(ctx context.Context, run *model.OrgSyncRun) (*model.OrgSyncRun, error) {
	s.logger.Info("starting lark sync")

	var (
		departmentsFetched  int
		departmentsUpserted int
		employeesFetched    int
		employeesUpserted   int
		errorCount          int
		errorMessage        string
		deptUpsertErrors    int
		empUpsertErrors     int
	)

	if s.larkClient == nil {
		errorCount++
		errorMessage = "lark client not configured"
		return s.finalizeRun(ctx, run, departmentsFetched, departmentsUpserted, employeesFetched, employeesUpserted, errorCount, errorMessage, fmt.Errorf("%s", errorMessage))
	}

	// Sync Departments
	s.logger.Info("fetching departments from lark")
	s.markStep(ctx, run, stepDepartmentsFetch, "running", 0, "", timePtr(time.Now().UTC()), nil)
	deptResps, err := s.larkClient.GetDepartments(ctx)
	if err != nil {
		s.markStep(ctx, run, stepDepartmentsFetch, "error", 0, err.Error(), nil, timePtr(time.Now().UTC()))
		errorCount++
		errorMessage = fmt.Sprintf("failed to fetch departments: %v", err)
		s.logger.Error("failed to fetch departments from lark", "error", err)
		return s.finalizeRun(ctx, run, departmentsFetched, departmentsUpserted, employeesFetched, employeesUpserted, errorCount, errorMessage, err)
	}

	s.logger.Info("received department responses from lark", "response_count", len(deptResps))
	deptIDSet := make(map[string]struct{})
	s.markStep(ctx, run, stepDepartmentsUpsert, "running", 0, "", timePtr(time.Now().UTC()), nil)
	for _, resp := range deptResps {
		s.logger.Info("processing department response", "items_count", len(resp.Data.Items), "has_more", resp.Data.HasMore)
		for _, item := range resp.Data.Items {
			if item.Status.IsDeleted {
				s.logger.Info("skipping deleted department", "name", item.Name, "id", item.OpenDepartmentID)
				continue
			}
			departmentsFetched++
			deptID := strings.TrimSpace(item.OpenDepartmentID)
			if deptID != "" {
				deptIDSet[deptID] = struct{}{}
			}
			dept := &model.Department{
				Name:             item.Name,
				LarkDepartmentID: item.OpenDepartmentID,
				ParentID:         item.ParentDepartmentID,
				LeaderID:         item.LeaderUserID,
				Status:           "active",
			}
			if err := s.deptRepo.Upsert(ctx, dept); err != nil {
				deptUpsertErrors++
				errorCount++
				if errorMessage == "" {
					errorMessage = fmt.Sprintf("failed to upsert department %q: %v", item.Name, err)
				}
				s.logger.Error("failed to upsert department", "error", err, "name", item.Name)
				continue
			}
			departmentsUpserted++
		}
	}
	s.markStep(ctx, run, stepDepartmentsFetch, "success", departmentsFetched, "", nil, timePtr(time.Now().UTC()))
	s.markStep(ctx, run, stepDepartmentsUpsert, stepStatusFor(departmentsFetched, deptUpsertErrors), departmentsUpserted, "", nil, timePtr(time.Now().UTC()))

	// Sync Employees
	s.logger.Info("fetching employees from lark")
	deptIDs := make([]string, 0, len(deptIDSet))
	for deptID := range deptIDSet {
		deptIDs = append(deptIDs, deptID)
	}
	s.markStep(ctx, run, stepEmployeesFetch, "running", 0, "", timePtr(time.Now().UTC()), nil)
	empResps, err := s.larkClient.GetEmployees(ctx, deptIDs)
	if err != nil {
		s.markStep(ctx, run, stepEmployeesFetch, "error", 0, err.Error(), nil, timePtr(time.Now().UTC()))
		errorCount++
		if errorMessage == "" {
			errorMessage = fmt.Sprintf("failed to fetch employees: %v", err)
		}
		s.logger.Error("failed to fetch employees from lark", "error", err)
		return s.finalizeRun(ctx, run, departmentsFetched, departmentsUpserted, employeesFetched, employeesUpserted, errorCount, errorMessage, err)
	}

	s.logger.Info("received employee responses from lark", "response_count", len(empResps))
	employeeByKey := make(map[string]*model.Employee)
	s.markStep(ctx, run, stepEmployeesUpsert, "running", 0, "", timePtr(time.Now().UTC()), nil)
	for _, resp := range empResps {
		s.logger.Info("processing employee response", "items_count", len(resp.Data.Items), "has_more", resp.Data.HasMore)
		for _, item := range resp.Data.Items {
			key := strings.TrimSpace(item.OpenID)
			if key == "" {
				key = strings.TrimSpace(item.Email)
			}
			if key == "" {
				continue
			}
			status := "active"
			if item.Status.IsFrozen || item.Status.IsResigned || item.Status.IsTerminated {
				status = "inactive"
			}

			deptID := ""
			if len(item.DepartmentIDs) > 0 {
				deptID = item.DepartmentIDs[0]
			}

			if existing, seen := employeeByKey[key]; seen {
				existing.DepartmentIDs = mergeDepartmentIDs(existing.DepartmentIDs, item.DepartmentIDs)
				if existing.DepartmentID == "" {
					existing.DepartmentID = deptID
				}
				if existing.Name == "" {
					existing.Name = item.Name
				}
				if existing.Email == "" {
					existing.Email = item.Email
				}
				if existing.LarkUserID == "" {
					existing.LarkUserID = item.OpenID
				}
				if existing.AvatarURL == "" {
					existing.AvatarURL = item.Avatar.Avatar72
				}
				if existing.JobTitle == "" {
					existing.JobTitle = item.JobTitle
				}
				if existing.Status == "" {
					existing.Status = status
				}
				continue
			}
			employeesFetched++
			employeeByKey[key] = &model.Employee{
				Name:          item.Name,
				Email:         item.Email,
				LarkUserID:    item.OpenID,
				DepartmentID:  deptID,
				DepartmentIDs: item.DepartmentIDs,
				Status:        status,
				AvatarURL:     item.Avatar.Avatar72,
				JobTitle:      item.JobTitle,
			}
		}
	}
	s.markStep(ctx, run, stepEmployeesFetch, "success", employeesFetched, "", nil, timePtr(time.Now().UTC()))
	for _, emp := range employeeByKey {
		if err := s.empRepo.Upsert(ctx, emp); err != nil {
			empUpsertErrors++
			errorCount++
			if errorMessage == "" {
				errorMessage = fmt.Sprintf("failed to upsert employee %q: %v", emp.Email, err)
			}
			s.logger.Error("failed to upsert employee", "error", err, "email", emp.Email)
			continue
		}
		employeesUpserted++
	}
	s.markStep(ctx, run, stepEmployeesUpsert, stepStatusFor(employeesFetched, empUpsertErrors), employeesUpserted, "", nil, timePtr(time.Now().UTC()))

	s.logger.Info("lark sync completed")
	return s.finalizeRun(ctx, run, departmentsFetched, departmentsUpserted, employeesFetched, employeesUpserted, errorCount, errorMessage, nil)
}

func (s *DepartmentService) GetDepartments(ctx context.Context) ([]model.Department, error) {
	return s.deptRepo.List(ctx)
}

func (s *DepartmentService) GetEmployeesByDepartment(ctx context.Context, departmentID string) ([]model.Employee, error) {
	emps, err := s.empRepo.ListByDepartment(ctx, departmentID)
	if err != nil {
		return nil, err
	}
	return s.enrichEmployees(ctx, emps), nil
}

func (s *DepartmentService) ListEmployees(ctx context.Context) ([]model.Employee, error) {
	if s.empRepo == nil {
		return []model.Employee{}, nil
	}
	emps, err := s.empRepo.List(ctx)
	if err != nil {
		return nil, err
	}
	return s.enrichEmployees(ctx, emps), nil
}

func (s *DepartmentService) GetEmployeeByID(ctx context.Context, id uint64) (*model.Employee, error) {
	if s.empRepo == nil {
		return nil, fmt.Errorf("employee repository not configured")
	}
	emp, err := s.empRepo.FindByID(ctx, id)
	if err != nil {
		return nil, err
	}
	employees := s.enrichEmployees(ctx, []model.Employee{*emp})
	if len(employees) == 0 {
		return emp, nil
	}
	return &employees[0], nil
}

func (s *DepartmentService) GetEmployeeByEmail(ctx context.Context, email string) (*model.Employee, error) {
	if s.empRepo == nil {
		return nil, fmt.Errorf("employee repository not configured")
	}
	emp, err := s.empRepo.FindByEmail(ctx, email)
	if err != nil {
		return nil, err
	}
	employees := s.enrichEmployees(ctx, []model.Employee{*emp})
	if len(employees) == 0 {
		return emp, nil
	}
	return &employees[0], nil
}

func (s *DepartmentService) FindEmployeesByEmails(ctx context.Context, emails []string) ([]model.Employee, error) {
	if s.empRepo == nil {
		return []model.Employee{}, nil
	}
	if len(emails) == 0 {
		return []model.Employee{}, nil
	}
	return s.empRepo.FindByEmails(ctx, emails)
}

func (s *DepartmentService) SearchEmployees(ctx context.Context, query string) ([]model.Employee, error) {
	emps, err := s.empRepo.Search(ctx, query)
	if err != nil {
		return nil, err
	}
	return s.enrichEmployees(ctx, emps), nil
}

func (s *DepartmentService) IsSyncRunning() bool {
	return s.isSyncRunning()
}

func (s *DepartmentService) ListSyncRuns(ctx context.Context, limit int) ([]model.OrgSyncRun, error) {
	if s.runRepo == nil {
		return []model.OrgSyncRun{}, nil
	}
	return s.runRepo.ListRecent(ctx, limit)
}

func (s *DepartmentService) LatestSuccessfulSync(ctx context.Context) (*model.OrgSyncRun, error) {
	if s.runRepo == nil {
		return nil, nil
	}
	return s.runRepo.LatestSuccessful(ctx)
}

func (s *DepartmentService) finalizeRun(
	ctx context.Context,
	run *model.OrgSyncRun,
	departmentsFetched int,
	departmentsUpserted int,
	employeesFetched int,
	employeesUpserted int,
	errorCount int,
	errorMessage string,
	syncErr error,
) (*model.OrgSyncRun, error) {
	if run == nil {
		if syncErr != nil {
			return nil, syncErr
		}
		return nil, nil
	}
	finishedAt := time.Now().UTC()
	run.FinishedAt = &finishedAt
	run.DurationMs = uint64(finishedAt.Sub(run.StartedAt).Milliseconds())
	run.DepartmentsFetched = departmentsFetched
	run.DepartmentsUpserted = departmentsUpserted
	run.EmployeesFetched = employeesFetched
	run.EmployeesUpserted = employeesUpserted
	run.ErrorCount = errorCount
	run.ErrorMessage = errorMessage
	if syncErr != nil {
		run.Status = "error"
	} else {
		run.Status = "success"
	}
	if s.runRepo != nil && run.ID != 0 {
		_ = s.runRepo.Update(ctx, run)
	}

	if syncErr != nil {
		return run, syncErr
	}
	return run, nil
}

func (s *DepartmentService) enrichEmployees(ctx context.Context, employees []model.Employee) []model.Employee {
	if len(employees) == 0 || s.deptRepo == nil {
		return employees
	}
	deptSet := make(map[string]struct{})
	for _, emp := range employees {
		if emp.DepartmentID != "" {
			deptSet[emp.DepartmentID] = struct{}{}
		}
		for _, deptID := range emp.DepartmentIDs {
			if deptID != "" {
				deptSet[deptID] = struct{}{}
			}
		}
	}
	if len(deptSet) == 0 {
		return employees
	}
	deptIDs := make([]string, 0, len(deptSet))
	for deptID := range deptSet {
		deptIDs = append(deptIDs, deptID)
	}
	depts, err := s.deptRepo.ListByLarkIDs(ctx, deptIDs)
	if err != nil {
		s.logger.Warn("failed to load departments for employee enrichment", "error", err)
		return employees
	}
	nameByID := make(map[string]string, len(depts))
	for _, dept := range depts {
		nameByID[dept.LarkDepartmentID] = dept.Name
	}
	for i := range employees {
		if employees[i].DepartmentID != "" {
			employees[i].DepartmentName = nameByID[employees[i].DepartmentID]
		}
		if len(employees[i].DepartmentIDs) > 0 {
			names := make(model.StringArray, 0, len(employees[i].DepartmentIDs))
			for _, deptID := range employees[i].DepartmentIDs {
				if name := nameByID[deptID]; name != "" {
					names = append(names, name)
				}
			}
			if len(names) > 0 {
				employees[i].DepartmentNames = names
			}
		}
	}
	return employees
}

func (s *DepartmentService) markStep(ctx context.Context, run *model.OrgSyncRun, step, status string, count int, errMsg string, startedAt, finishedAt *time.Time) {
	if run == nil {
		return
	}
	if run.StepStatus == nil {
		run.StepStatus = model.JSONB{}
	}
	existing, _ := run.StepStatus[step].(map[string]interface{})
	payload := map[string]interface{}{
		"status": status,
	}
	if count >= 0 {
		payload["count"] = count
	}
	if errMsg != "" {
		payload["error"] = errMsg
	}
	if startedAt != nil {
		payload["started_at"] = startedAt
	} else if existing != nil {
		if value, ok := existing["started_at"]; ok {
			payload["started_at"] = value
		}
	}
	if finishedAt != nil {
		payload["finished_at"] = finishedAt
	} else if existing != nil {
		if value, ok := existing["finished_at"]; ok {
			payload["finished_at"] = value
		}
	}
	run.StepStatus[step] = payload
	if s.runRepo != nil {
		_ = s.runRepo.Update(ctx, run)
	}
}

func (s *DepartmentService) tryStartSync() bool {
	s.syncMu.Lock()
	defer s.syncMu.Unlock()
	if s.syncRunning {
		return false
	}
	s.syncRunning = true
	return true
}

func (s *DepartmentService) finishSync() {
	s.syncMu.Lock()
	defer s.syncMu.Unlock()
	s.syncRunning = false
}

func (s *DepartmentService) isSyncRunning() bool {
	s.syncMu.Lock()
	defer s.syncMu.Unlock()
	return s.syncRunning
}

func mergeDepartmentIDs(current model.StringArray, incoming []string) model.StringArray {
	seen := make(map[string]struct{}, len(current)+len(incoming))
	out := make(model.StringArray, 0, len(current)+len(incoming))
	for _, id := range current {
		trimmed := strings.TrimSpace(id)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	for _, id := range incoming {
		trimmed := strings.TrimSpace(id)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	return out
}

func stepStatusFor(total, errors int) string {
	if errors <= 0 {
		return "success"
	}
	if total > errors {
		return "partial"
	}
	return "error"
}

func timePtr(t time.Time) *time.Time {
	return &t
}
