package lib

import (
	"encoding/json"
	"errors"
	"strings"
)

var (
	ErrAccessDenied    = errors.New("access denied")
	ErrInsufficientOps = errors.New("insufficient permissions")
)

// TemplateProjectID is the well-known project_id for global request templates.
// All authenticated users have full rwdm access to this "project".
const TemplateProjectID = "00000000-0000-0000-0000-000000000000"

// CheckAccess verifies that actorID has the required operation on the given project/entry.
// op is one of "r", "w", "d", "m" (read, write, delete, manage).
// Returns the matching Access grant or an error.
//
// SECURITY: Org membership does NOT grant project access — only explicit access grants count.
// The access table is the SOLE source of truth for project permissions.
// deal_org entries are for domain validation during invite creation only.
func CheckAccess(db *DB, actorID, projectID string, workstreamID string, op string) (*Access, error) {
	// super_admin bypasses all access checks — full rwdm on everything
	if isSA, _ := IsSuperAdmin(db, actorID); isSA {
		return &Access{
			UserID:    actorID,
			ProjectID: projectID,
			Role:      RoleSuperAdmin,
			Ops:       "rwdm",
		}, nil
	}

	// Template project — all authenticated users have full access
	if projectID == TemplateProjectID && actorID != "" {
		return &Access{
			UserID:    actorID,
			ProjectID: TemplateProjectID,
			Role:      RoleIBMember,
			Ops:       "rwdm",
		}, nil
	}

	grants, err := getUserAccess(db, actorID, projectID)
	if err != nil {
		return nil, err
	}

	for _, g := range grants {
		if g.RevokedAt != nil {
			continue
		}
		// Grant matches if it covers all workstreams or the specific one
		if g.WorkstreamID != "" && g.WorkstreamID != workstreamID && workstreamID != "" {
			continue
		}
		if strings.Contains(g.Ops, op) {
			return &g, nil
		}
	}

	return nil, ErrAccessDenied
}

// CheckAccessRead is a convenience for read operations.
func CheckAccessRead(db *DB, actorID, projectID, workstreamID string) error {
	_, err := CheckAccess(db, actorID, projectID, workstreamID, "r")
	return err
}

// CheckAccessWrite is a convenience for write operations.
func CheckAccessWrite(db *DB, actorID, projectID, workstreamID string) error {
	_, err := CheckAccess(db, actorID, projectID, workstreamID, "w")
	return err
}

// CheckAccessDelete is a convenience for delete operations.
func CheckAccessDelete(db *DB, actorID, projectID, workstreamID string) error {
	_, err := CheckAccess(db, actorID, projectID, workstreamID, "d")
	return err
}

// IsSuperAdmin returns true if the user has super_admin role on any project.
func IsSuperAdmin(db *DB, userID string) (bool, error) {
	var count int
	err := db.Conn.QueryRow(
		`SELECT COUNT(*) FROM access WHERE user_id = ? AND role = ? AND revoked_at IS NULL`,
		userID, RoleSuperAdmin,
	).Scan(&count)
	return count > 0, err
}

// IsBuyerRole returns true if the role is a buyer role.
// Buyers cannot see pre_dataroom entries.
func IsBuyerRole(role string) bool {
	return role == RoleBuyerAdmin || role == RoleBuyerMember
}

// CanGrantRole checks whether the granting role can grant the target role.
// A role can only grant roles at or below its own hierarchy level.
func CanGrantRole(granterRole, targetRole string) bool {
	granterLevel, ok1 := RoleHierarchy[granterRole]
	targetLevel, ok2 := RoleHierarchy[targetRole]
	if !ok1 || !ok2 {
		return false
	}
	return granterLevel >= targetLevel
}

// ResolveWorkstreamID walks up the entry tree to find the workstream ancestor.
// Returns empty string if the entry is at project level (depth 0).
func ResolveWorkstreamID(db *DB, entry *Entry) (string, error) {
	if entry.Type == TypeWorkstream {
		return entry.EntryID, nil
	}
	if entry.Type == TypeProject || entry.Depth == 0 {
		return "", nil
	}

	// Walk up to find workstream
	current := entry
	for current.Depth > 1 && current.ParentID != "" {
		parent, err := entryReadSystem(db, current.ParentID)
		if err != nil {
			return "", err
		}
		if parent == nil {
			return "", nil
		}
		if parent.Type == TypeWorkstream {
			return parent.EntryID, nil
		}
		current = parent
	}
	return current.EntryID, nil
}

// getUserAccess retrieves all active access grants for a user on a project.
func getUserAccess(db *DB, userID, projectID string) ([]Access, error) {
	rows, err := db.Conn.Query(
		`SELECT id, project_id, workstream_id, user_id, role, ops, can_grant,
		        granted_by, granted_at, revoked_at, revoked_by
		 FROM access
		 WHERE user_id = ? AND project_id = ? AND revoked_at IS NULL`,
		userID, projectID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var grants []Access
	for rows.Next() {
		var a Access
		var wsID *string
		var revokedAt *int64
		var revokedBy *string
		err := rows.Scan(&a.ID, &a.ProjectID, &wsID, &a.UserID, &a.Role,
			&a.Ops, &a.CanGrant, &a.GrantedBy, &a.GrantedAt, &revokedAt, &revokedBy)
		if err != nil {
			return nil, err
		}
		if wsID != nil {
			a.WorkstreamID = *wsID
		}
		a.RevokedAt = revokedAt
		if revokedBy != nil {
			a.RevokedBy = *revokedBy
		}
		grants = append(grants, a)
	}
	return grants, rows.Err()
}

// GetUserHighestRole returns the highest-privilege role a user has on a project.
func GetUserHighestRole(db *DB, userID, projectID string) (string, error) {
	grants, err := getUserAccess(db, userID, projectID)
	if err != nil {
		return "", err
	}

	bestRole := ""
	bestLevel := -1
	for _, g := range grants {
		if level, ok := RoleHierarchy[g.Role]; ok && level > bestLevel {
			bestLevel = level
			bestRole = g.Role
		}
	}
	if bestRole == "" {
		return "", ErrAccessDenied
	}
	return bestRole, nil
}

var (
	ErrDomainMismatch = errors.New("email domain does not match organization requirements")
)


// ValidateOrgDomain checks if an email is ELIGIBLE to be invited to a specific role in a deal.
// This is called ONLY during invite creation for domain validation.
//
// SECURITY: This function answers "is this email allowed to be invited?" — it does NOT grant
// any access whatsoever. Access is granted only by explicit rows in the access table.
// Example: PwC (@pwc.com) may be a buyer in Project A and seller in Project B. A PwC partner
// invited to Project A has ZERO visibility into Project B, even though PwC is listed there.
//
// Returns nil if validation passes (email domain matches or no domain_lock is enforced).
func ValidateOrgDomain(db *DB, cfg *Config, projectID, email, role string) error {
	// Find all deal_org entries for the project
	rows, err := db.Conn.Query(
		`SELECT entry_id, data FROM entries 
		 WHERE project_id = ? AND type = ? AND deleted_at IS NULL`,
		projectID, TypeDealOrg,
	)
	if err != nil {
		return err
	}
	defer rows.Close()

	// Get the project key for decryption
	projectKey, err := DeriveProjectKey(cfg.MasterKey, projectID)
	if err != nil {
		return err
	}

	// Extract email domain
	parts := strings.Split(email, "@")
	if len(parts) != 2 {
		return errors.New("invalid email format")
	}
	emailDomain := strings.ToLower(parts[1])

	foundMatchingRole := false
	for rows.Next() {
		var entryID string
		var dataBlob []byte
		if err := rows.Scan(&entryID, &dataBlob); err != nil {
			return err
		}

		// Decrypt and parse deal_org data
		if len(dataBlob) == 0 {
			continue
		}
		dataText, err := Unpack(projectKey, dataBlob)
		if err != nil {
			continue
		}

		var dealOrgData DealOrgData
		if err := json.Unmarshal([]byte(dataText), &dealOrgData); err != nil {
			continue
		}

		// Only check deal_orgs with matching role
		if dealOrgData.Role != role {
			continue
		}
		foundMatchingRole = true

		// If domain lock is disabled, pass through
		if !dealOrgData.DomainLock {
			continue
		}

		// Get the organization entry to check domains
		orgEntry, err := entryReadSystem(db, dealOrgData.OrgID)
		if err != nil || orgEntry == nil {
			continue
		}

		// Decrypt org data
		orgKey, err := DeriveProjectKey(cfg.MasterKey, orgEntry.ProjectID)
		if err != nil {
			continue
		}
		if len(orgEntry.Data) == 0 {
			continue
		}
		orgDataText, err := Unpack(orgKey, orgEntry.Data)
		if err != nil {
			continue
		}

		var orgData OrgData
		if err := json.Unmarshal([]byte(orgDataText), &orgData); err != nil {
			continue
		}

		// Check if email domain matches any of the org's domains
		for _, domain := range orgData.Domains {
			if strings.ToLower(domain) == emailDomain {
				return nil // Match found, validation passes
			}
		}
	}

	// If no deal_orgs with matching role and domain_lock=true, pass
	if !foundMatchingRole {
		return nil
	}

	// Check if ALL deal_orgs with this role have domain_lock=false
	// If we got here and foundMatchingRole=true, that means there's at least one
	// with domain_lock=true that didn't match, so return error
	return ErrDomainMismatch
}
