/**
 * CoverDetector.js
 * Handles the logic for detecting cover between tokens or points
 */

import FeatsHandler from '../../chat/services/FeatsHandler.js';
import { MODULE_ID } from '../../constants.js';
// Removed unused imports that were only used by the removed center intersection mode
import {
  intersectsBetweenTokens,
  segmentRectIntersectionLength,
} from '../../helpers/line-intersection.js';
import { CoverRegionBehavior } from '../../regions/CoverRegionBehavior.js';
import {
  getSizeRank,
  getTokenCorners,
  getTokenRect,
  getTokenVerticalSpanFt,
} from '../../helpers/size-elevation-utils.js';
import { doesWallBlockAtElevation, doesWallBlockLineOfSight } from '../../helpers/wall-height-utils.js';
import { RuleElementCoverService } from '../../rule-elements/RuleElementCoverService.js';
import { LevelsIntegration } from '../../services/LevelsIntegration.js';

import { getVisibilityBetween } from '../../utils.js';
import { getLogger } from '../../utils/logger.js';

export class CoverDetector {
  constructor() {
    this._featUpgradeRecords = new Map();
    this._ruleElementBlocks = new Map();
    this._snipingDuoRecords = new Map();
  }
  // Define token disposition constants for use within this class
  static TOKEN_DISPOSITIONS = {
    FRIENDLY: 1,
    NEUTRAL: 0,
    HOSTILE: -1,
  };
  /**
   * Detect cover using an arbitrary origin point instead of an attacker token
   * @param {Object} origin - Point with x,y coordinates
   * @param {Object} target - Target token
   * @param {Object} options - Additional options
   * @returns {string} Cover state ('none', 'lesser', 'standard', 'greater')
   */
  detectFromPoint(origin, target, options = {}) {
    try {
      if (!origin || !target) return 'none';

      // Build a minimal attacker-like object with a center at the origin point
      const pseudoAttacker = {
        id: 'template-origin',
        center: { x: Number(origin.x) || 0, y: Number(origin.y) || 0 },
        getCenterPoint: () => ({
          x: Number(origin.x) || 0,
          y: Number(origin.y) || 0,
          elevation: 0,
        }),
        actor: null,
        document: { x: origin.x, y: origin.y, width: 0, height: 0 },
      };

      // Reuse the normal path using the pseudo attacker
      return this.detectBetweenTokens(pseudoAttacker, target, options);
    } catch (error) {
      console.error('PF2E Visioner | CoverDetector.detectFromPoint error:', error);
      return 'none';
    }
  }

  /**
   * Detect cover state for an attack between two tokens
   * @param {Object} attacker - Attacker token
   * @param {Object} target - Target token
   * @param {Object} options - Additional options (attackContext for rule element checks)
   * @returns {string} Cover state ('none', 'lesser', 'standard', 'greater')
   */
  detectBetweenTokens(attacker, target, options = {}) {
    try {
      if (!attacker || !target) return 'none';

      // Exclude same token (attacker and target are the same)
      if (attacker.id === target.id) return 'none';

      try {
        const ruleElementCover = this._checkRuleElementCover(attacker, target);
        if (ruleElementCover) {
          return ruleElementCover;
        }
      } catch (error) {
        console.warn('PF2E Visioner | Error checking rule element cover:', error);
      }

      const p1 = attacker.center ?? attacker.getCenterPoint();
      const p2 = target.center ?? target.getCenterPoint();

      // Calculate elevation spans for 3D wall height checks
      let elevationRange = null;
      let attackerSpan = null;
      let targetSpan = null;
      try {
        attackerSpan = getTokenVerticalSpanFt(attacker);
        targetSpan = getTokenVerticalSpanFt(target);
        elevationRange = {
          bottom: Math.min(attackerSpan.bottom, targetSpan.bottom),
          top: Math.max(attackerSpan.top, targetSpan.top),
        };
      } catch (error) {
        // If we can't get elevation, don't filter by it
      }

      // FIRST: Check for wall cover overrides - these take precedence regardless of natural blocking
      const wallOverride = this._checkWallCoverOverrides(p1, p2, elevationRange);
      if (wallOverride !== null) {
        let adjusted = this._applyLevelsCoverAdjustment(attacker, target, wallOverride);
        const allowGreaterOverride = !!game.settings.get('pf2e-visioner', 'wallCoverAllowGreater');
        if (!allowGreaterOverride && adjusted === 'greater') {
          adjusted = 'standard';
        }
        return adjusted;
      }

      // Check if there's any blocking terrain (walls) in the way
      const segmentAnalysis = this._analyzeSegmentObstructions(p1, p2, elevationRange, attackerSpan, targetSpan);
      const hasWallsInTheWay = segmentAnalysis.hasBlockingTerrain;

      // NEW LOGIC: Priority based on wall presence
      let calculatedCover;
      if (!hasWallsInTheWay) {
        // Case 1: No walls in the way - use token cover
        const intersectionMode = this._getIntersectionMode();
        const filters = { ...this._getAutoCoverFilterSettings(attacker), attackContext: options.attackContext };
        let blockers = this._getEligibleBlockingTokens(attacker, target, filters);

        // Apply elevation filtering (mode-aware)
        blockers = this._filterBlockersByElevation(attacker, target, blockers, intersectionMode);

        // Determine token cover based on intersection mode
        let tokenCover;
        if (intersectionMode === 'tactical') {
          tokenCover = this._evaluateCoverByTactical(attacker, target, blockers, elevationRange, options.attackContext, attackerSpan, targetSpan);
        } else if (intersectionMode === 'coverage') {
          tokenCover = this._evaluateCoverByCoverage(attacker, target, blockers, options.attackContext);
        } else {
          tokenCover = this._evaluateCreatureSizeCover(attacker, target, blockers, options.attackContext);
        }

        // Apply token cover overrides
        tokenCover = this._applyTokenCoverOverrides(attacker, target, blockers, tokenCover);

        // Apply Levels integration for elevation-based cover adjustment
        tokenCover = this._applyLevelsCoverAdjustment(attacker, target, tokenCover);

        calculatedCover = tokenCover;
      } else {
        // Case 2: There IS a wall in the way - use new wall cover rules
        let wallCover = this._evaluateWallsCover(p1, p2, elevationRange, attackerSpan, targetSpan);

        wallCover = this._applyLevelsCoverAdjustment(attacker, target, wallCover);

        if (wallCover === 'lesser') {
          wallCover = 'standard';
        }

        const allowGreater = !!game.settings.get('pf2e-visioner', 'wallCoverAllowGreater');
        if (!allowGreater && wallCover === 'greater') {
          wallCover = 'standard';
        }

        calculatedCover = wallCover;
      }

      return this._applyRegionCover(p1, p2, calculatedCover);
    } catch (error) {
      console.error('PF2E Visioner | CoverDetector.detectForAttack error:', error);
      return 'none';
    }
  }

  /**
   * Get the intersection mode from settings
   * @returns {string}
   * @private
   */
  _getIntersectionMode() {
    try {
      const mode = game.settings.get('pf2e-visioner', 'autoCoverTokenIntersectionMode');
      return mode || 'tactical';
    } catch (error) {
      console.warn(
        'PF2E Visioner | Could not read autoCoverTokenIntersectionMode setting, using default',
        error,
      );
      return 'tactical';
    }
  }

  /**
   * Get auto cover filter settings
   * @param {Object} attacker
   * @returns {Object}
   * @private
   */
  _getAutoCoverFilterSettings(attacker) {
    const ignoreUndetected = !!game.settings?.get?.(MODULE_ID, 'autoCoverIgnoreUndetected');
    const ignoreDead = !!game.settings?.get?.(MODULE_ID, 'autoCoverIgnoreDead');
    const ignoreAllies = !!game.settings?.get?.(MODULE_ID, 'autoCoverIgnoreAllies');
    const allowProneBlockers = !!game.settings?.get?.(MODULE_ID, 'autoCoverAllowProneBlockers');
    const ignoreSmallerTokens = !!game.settings?.get?.(MODULE_ID, 'autoCoverIgnoreSmallerTokens');
    const ignoreSameSizeTokens = !!game.settings?.get?.(MODULE_ID, 'autoCoverIgnoreSameSizeTokens');
    const ignoreLargerTokens = !!game.settings?.get?.(MODULE_ID, 'autoCoverIgnoreLargerTokens');

    return {
      ignoreUndetected,
      ignoreDead,
      ignoreAllies,
      allowProneBlockers,
      ignoreSmallerTokens,
      ignoreSameSizeTokens,
      ignoreLargerTokens,
      attackerAlliance: attacker?.actor?.alliance,
      attackerSizeRank: getSizeRank(attacker),
    };
  }

  /**
   * Check if a wall would naturally block from a given direction (without considering overrides)
   * Checks door state and wall directionality
   * @param {Object} wallDoc - Wall document
   * @param {Object} attackerPos - Attacker position {x, y}
   * @returns {boolean} True if wall would naturally block from attacker position
   * @private
   */
  _wouldWallNaturallyBlock(wallDoc, attackerPos) {
    try {
      if (wallDoc.sight === 0) return false;

      const isDoor = Number(wallDoc.door) > 0;
      const doorState = Number(wallDoc.ds ?? wallDoc.doorState ?? 0);

      let doorAllowsBlocking = true;
      if (isDoor && doorState === 1) {
        doorAllowsBlocking = false;
      }

      let directionAllowsBlocking = true;
      if (wallDoc.dir != null && typeof wallDoc.dir === 'number') {
        if (wallDoc.dir === 0) {
          directionAllowsBlocking = true;
        } else {
          const [x1, y1, x2, y2] = Array.isArray(wallDoc.c)
            ? wallDoc.c
            : [wallDoc.x, wallDoc.y, wallDoc.x2, wallDoc.y2];

          const wallDx = x2 - x1;
          const wallDy = y2 - y1;
          const attackerDx = attackerPos.x - x1;
          const attackerDy = attackerPos.y - y1;
          const crossProduct = wallDx * attackerDy - wallDy * attackerDx;

          if (wallDoc.dir === 1) {
            directionAllowsBlocking = crossProduct < 0;
          } else if (wallDoc.dir === 2) {
            directionAllowsBlocking = crossProduct > 0;
          }
        }
      }

      return doorAllowsBlocking && directionAllowsBlocking;
    } catch {
      return true;
    }
  }

  /**
   * Check if a wall blocks sight from a given direction based on its sight settings
   * A wall blocks only if BOTH door state AND direction allow blocking, then applies cover overrides only when wall would naturally block
   * @param {Object} wallDoc - Wall document
   * @param {Object} attackerPos - Attacker position {x, y}
   * @returns {boolean} True if wall blocks sight from attacker position
   * @private
   */
  _doesWallBlockFromDirection(wallDoc, attackerPos) {
    try {
      const wouldNaturallyBlock = this._wouldWallNaturallyBlock(wallDoc, attackerPos);

      const coverOverride = wallDoc.getFlag?.(MODULE_ID, 'coverOverride');
      if (coverOverride && coverOverride !== 'auto') {
        if (wouldNaturallyBlock) {
          if (coverOverride === 'none') return false;
          return true;
        }
        return false;
      }

      return wouldNaturallyBlock;
    } catch (error) {
      console.warn('PF2E Visioner | Error checking wall direction:', error);
      return true;
    }
  }

  /**
   * Apply Levels module elevation-based cover adjustment
   * @param {Object} attacker - Attacker token
   * @param {Object} target - Target token
   * @param {string} baseCover - Base cover level before elevation adjustment
   * @returns {string} Adjusted cover level
   * @private
   */
  _applyLevelsCoverAdjustment(attacker, target, baseCover) {
    try {
      const levelsIntegration = LevelsIntegration.getInstance();
      if (!levelsIntegration.isActive) {
        return baseCover;
      }

      const adjusted = levelsIntegration.adjustCoverForElevation(attacker, target, baseCover);
      if (adjusted === 'greater' && baseCover !== 'greater') {
        const allowGreater = !!game.settings.get('pf2e-visioner', 'wallCoverAllowGreater');
        if (!allowGreater) {
          return 'standard';
        }
      }
      return adjusted;
    } catch (error) {
      console.warn('PF2E Visioner | Error applying Levels cover adjustment:', error);
      return baseCover;
    }
  }

  _applyRegionCover(observerPoint, targetPoint, calculatedCover) {
    try {
      const regionCover = CoverRegionBehavior.getCoverBetween(observerPoint, targetPoint);
      if (regionCover) {
        const coverOrder = ['none', 'lesser', 'standard', 'greater'];
        if (coverOrder.indexOf(regionCover) > coverOrder.indexOf(calculatedCover)) {
          return regionCover;
        }
      }
      return calculatedCover;
    } catch (error) {
      console.warn('PF2E Visioner | Error checking region cover:', error);
      return calculatedCover;
    }
  }

  /**
   * Evaluate walls cover using center-to-center segment analysis
   * @param {Object} p1 - Attacker center point
   * @param {Object} p2 - Target center point
   * @param {Object} elevationRange - Optional elevation range {bottom, top} for wall height filtering
   * @returns {string} Cover category ('none', 'lesser', 'standard', 'greater')
   * @private
   */
  _evaluateWallsCover(p1, p2, elevationRange = null, attackerSpan = null, targetSpan = null) {
    if (!canvas?.walls) return 'none';

    // First check for manual wall cover overrides - if present, use it directly and skip all other checks
    const wallOverride = this._checkWallCoverOverrides(p1, p2, elevationRange, attackerSpan, targetSpan);
    if (wallOverride !== null) {
      return wallOverride;
    }

    // Analyze the center-to-center segment
    const segmentAnalysis = this._analyzeSegmentObstructions(p1, p2, elevationRange, attackerSpan, targetSpan);

    // Determine cover category based on new rules
    let coverCategory = 'none';

    // Rule 1: No obstructions
    if (!segmentAnalysis.hasBlockingTerrain && !segmentAnalysis.hasCreatures) {
      coverCategory = 'none';
    }
    // Rule 2: Creature space only, no blocking terrain
    else if (!segmentAnalysis.hasBlockingTerrain && segmentAnalysis.hasCreatures) {
      coverCategory = 'standard';
    }
    // Rule 3: Any blocking terrain
    else if (segmentAnalysis.hasBlockingTerrain) {
      // Calculate wall coverage percentage using existing method
      const target = this._findNearestTokenToPoint(p2);
      let wallCoveragePercent = 0;

      if (target) {
        wallCoveragePercent = this._estimateWallCoveragePercent(p1, target, elevationRange, attackerSpan, targetSpan);
      }

      // Get threshold settings
      const stdThreshold = Math.max(
        0,
        Number(game.settings.get('pf2e-visioner', 'wallCoverStandardThreshold') ?? 50),
      );
      const grtThreshold = Math.max(
        0,
        Number(game.settings.get('pf2e-visioner', 'wallCoverGreaterThreshold') ?? 70),
      );
      const allowGreater = !!game.settings.get('pf2e-visioner', 'wallCoverAllowGreater');

      // Determine cover level based on coverage percentage
      if (allowGreater && wallCoveragePercent >= grtThreshold) {
        coverCategory = 'greater';
      } else if (wallCoveragePercent >= stdThreshold) {
        coverCategory = 'standard';
      } else {
        // Fallback for cases where coverage calculation fails but walls are detected
        coverCategory = 'standard';
      }
    }

    return coverCategory;
  }

  /**
   * Analyze what obstructions the center-to-center segment passes through
   * @param {Object} p1 - Start point (attacker center)
   * @param {Object} p2 - End point (target center)
   * @param {Object} elevationRange - Optional elevation range {bottom, top} for wall height filtering
   * @returns {Object} Analysis object with obstruction details
   * @private
   */
  _analyzeSegmentObstructions(p1, p2, elevationRange = null, attackerSpan = null, targetSpan = null) {
    const segmentLength = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);

    const analysis = {
      hasBlockingTerrain: false,
      hasCreatures: false,
      blockingWalls: [],
      intersectingCreatures: [],
      totalBlockedLength: 0,
      segmentLength: segmentLength,
    };

    // Check for blocking walls/terrain
    let skipWallCheck = false;
    if (attackerSpan && targetSpan) {
      const levelsIntegration = LevelsIntegration.getInstance();
      if (levelsIntegration.isActive) {
        const p0_3d = { x: p1.x, y: p1.y, z: (attackerSpan.bottom + attackerSpan.top) / 2 };
        const p1_3d = { x: p2.x, y: p2.y, z: (targetSpan.bottom + targetSpan.top) / 2 };
        skipWallCheck = !levelsIntegration.test3DPointCollision(p0_3d, p1_3d, 'sight');
      }
    }

    if (!skipWallCheck) {
      const walls = canvas?.walls?.objects?.children || [];
      for (const wall of walls) {
        const wallDoc = wall.document || wall;

        if (!this._doesWallBlockFromDirection(wallDoc, p1)) continue;

        const coords = wall?.coords;
        if (!coords) continue;

        const intersection = this._lineIntersectionPoint(
          p1.x,
          p1.y,
          p2.x,
          p2.y,
          coords[0],
          coords[1],
          coords[2],
          coords[3],
        );

        if (intersection) {
          if (attackerSpan && targetSpan && segmentLength > 0) {
            const t = Math.sqrt((intersection.x - p1.x) ** 2 + (intersection.y - p1.y) ** 2) / segmentLength;
            if (!doesWallBlockLineOfSight(wallDoc, attackerSpan, targetSpan, t)) {
              continue;
            }
          } else if (elevationRange && !doesWallBlockAtElevation(wallDoc, elevationRange)) {
            continue;
          }

          analysis.hasBlockingTerrain = true;
          analysis.blockingWalls.push({
            wall: wallDoc,
            intersection: intersection,
            coords: coords,
          });
        }
      }
    }

    // Check for creatures along the segment
    const tokens = canvas?.tokens?.placeables || [];
    for (const token of tokens) {
      if (!token?.actor) continue;

      // Skip the attacker and target themselves
      if (this._isPointNearToken(p1, token) || this._isPointNearToken(p2, token)) continue;

      const tokenRect = getTokenRect(token);
      const intersectionLength = segmentRectIntersectionLength(p1, p2, tokenRect);

      if (intersectionLength > 0) {
        analysis.hasCreatures = true;
        analysis.intersectingCreatures.push({
          token: token,
          intersectionLength: intersectionLength,
        });
        analysis.totalBlockedLength += intersectionLength;
      }
    }

    // Calculate total blocked length from walls
    // For walls crossing the segment, estimate the blocked portion based on wall pattern density
    if (analysis.blockingWalls.length > 0) {
      // If there are multiple walls or walls seem to form a substantial barrier,
      // we'll let the substantial obstruction logic handle this
      // For now, just track that walls exist
    }

    return analysis;
  }

  /**
   * Check if a point is near a token's center (within token bounds)
   * @param {Object} point - Point to check
   * @param {Object} token - Token to check against
   * @returns {boolean} True if point is within token bounds
   * @private
   */
  _isPointNearToken(point, token) {
    try {
      const tokenRect = getTokenRect(token);
      return (
        point.x >= tokenRect.x1 &&
        point.x <= tokenRect.x2 &&
        point.y >= tokenRect.y1 &&
        point.y <= tokenRect.y2
      );
    } catch {
      return false;
    }
  }

  /**
   * Check for manual wall cover overrides along the line of sight
   * Overrides that grant cover (lesser/standard/greater) apply if wall intersects line, regardless of natural blocking
   * Override 'none' only applies if wall would naturally block (to remove natural cover)
   * @param {Object} p1 - Start point
   * @param {Object} p2 - End point
   * @param {Object} elevationRange - Optional elevation range {bottom, top} for wall height filtering
   * @returns {string|null} Cover override ('none', 'lesser', 'standard', 'greater') or null if no override
   * @private
   */
  _checkWallCoverOverrides(p1, p2, elevationRange = null, attackerSpan = null, targetSpan = null) {
    try {
      const ray = this._createRay(p1, p2);
      const rayLength = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
      const walls = canvas.walls.objects?.children || [];

      let highestCover = null;
      const coverOrder = ['none', 'lesser', 'standard', 'greater'];

      for (const wall of walls) {
        const wallDoc = wall.document || wall;
        const coverOverride = wallDoc.getFlag?.(MODULE_ID, 'coverOverride');

        if (!coverOverride || coverOverride === 'auto') {
          continue;
        }

        const coords = wall?.coords;
        if (!coords) {
          continue;
        }

        const intersection = this._lineIntersectionPoint(
          ray.A.x,
          ray.A.y,
          ray.B.x,
          ray.B.y,
          coords[0],
          coords[1],
          coords[2],
          coords[3],
        );

        if (!intersection) {
          continue;
        }

        if (attackerSpan && targetSpan && rayLength > 0) {
          const t = Math.sqrt((intersection.x - p1.x) ** 2 + (intersection.y - p1.y) ** 2) / rayLength;
          if (!doesWallBlockLineOfSight(wallDoc, attackerSpan, targetSpan, t)) {
            continue;
          }
        } else if (elevationRange && !doesWallBlockAtElevation(wallDoc, elevationRange)) {
          continue;
        }

        const wouldNaturallyBlock = this._wouldWallNaturallyBlock(wallDoc, p1);

        // For 'none' override, only apply if wall would naturally block (to remove natural cover)
        if (coverOverride === 'none') {
          if (!wouldNaturallyBlock) {
            continue;
          }
        }
        // For cover-granting overrides (lesser/standard/greater), apply if wall intersects line
        // regardless of whether it would naturally block

        if (highestCover === null) {
          highestCover = coverOverride;
        } else {
          const coverIndex = coverOrder.indexOf(coverOverride);
          const currentIndex = coverOrder.indexOf(highestCover);

          if (coverIndex > currentIndex) {
            highestCover = coverOverride;
          }
        }
      }

      return highestCover;
    } catch (error) {
      console.error('PF2E Visioner | _checkWallCoverOverrides error', error);
      return null;
    }
  }

  /**
   * Find nearest token to a point (screen coords). Best-effort helper for wall coverage.
   */
  _findNearestTokenToPoint(p) {
    try {
      const tokens = canvas?.tokens?.placeables || [];
      let best = null;
      let bestD = Infinity;
      for (const t of tokens) {
        const c = t.center ?? t.getCenterPoint?.();
        if (!c) continue;
        const dx = c.x - p.x;
        const dy = c.y - p.y;
        const d = dx * dx + dy * dy;
        if (d < bestD) {
          bestD = d;
          best = t;
        }
      }
      return best;
    } catch {
      return null;
    }
  }

  /**
   * Estimate percent of the target token's edge directions that are blocked by walls from origin p1.
   * Samples multiple points along the target perimeter and casts rays to each, counting wall collisions.
   * Uses a more accurate D&D/PF2e style approach based on corner-to-corner line blocking.
   * @param {Object} p1 - Start point
   * @param {Object} target - Target token
   * @param {Object} elevationRange - Optional elevation range {bottom, top} for wall height filtering
   * @returns {number} Percentage of blocked sight lines (0-100)
   * @private
   */
  _estimateWallCoveragePercent(p1, target, elevationRange = null, attackerSpan = null, targetSpan = null) {
    try {
      const rect = getTokenRect(target);

      // Sample points around the target's perimeter more densely for accuracy
      const points = [];
      const samplePerEdge = 5; // Increased sampling for better accuracy

      // Helper to interpolate points along an edge
      const pushLerp = (ax, ay, bx, by) => {
        for (let i = 0; i <= samplePerEdge; i++) {
          const t = i / samplePerEdge;
          points.push({ x: ax + (bx - ax) * t, y: ay + (by - ay) * t });
        }
      };

      // Add the four corners explicitly (most important for D&D/PF2e rules)
      points.push({ x: rect.x1, y: rect.y1 }); // Top-left corner
      points.push({ x: rect.x2, y: rect.y1 }); // Top-right corner
      points.push({ x: rect.x2, y: rect.y2 }); // Bottom-right corner
      points.push({ x: rect.x1, y: rect.y2 }); // Bottom-left corner

      // Add center point for additional context
      const targetCenter = { x: (rect.x1 + rect.x2) / 2, y: (rect.y1 + rect.y2) / 2 };
      points.push(targetCenter);

      // Sample edges: top, right, bottom, left
      pushLerp(rect.x1, rect.y1, rect.x2, rect.y1); // Top edge
      pushLerp(rect.x2, rect.y1, rect.x2, rect.y2); // Right edge
      pushLerp(rect.x2, rect.y2, rect.x1, rect.y2); // Bottom edge
      pushLerp(rect.x1, rect.y2, rect.x1, rect.y1); // Left edge

      // Count blocked sight lines
      let blocked = 0;
      for (const pt of points) {
        if (this._isRayBlockedByWalls(p1, pt, elevationRange, attackerSpan, targetSpan)) blocked++;
      }

      // Calculate raw percentage
      const rawPct = (blocked / Math.max(1, points.length)) * 100;

      // Remove the arbitrary center weight reduction - let the actual blockage speak for itself
      // This provides more intuitive and predictable cover calculations
      return rawPct;
    } catch {
      return 0;
    }
  }

  /**
   * Check if a ray from point A to point B is blocked by walls
   * Only counts walls that are actually between the points, not beyond point B
   * @param {Object} a - Start point {x, y}
   * @param {Object} b - End point {x, y}
   * @param {Object} elevationRange - Optional elevation range {bottom, top} to check against Wall Height module
   * @returns {boolean} True if ray is blocked by walls
   * @private
   */
  _isRayBlockedByWalls(a, b, elevationRange = null, attackerSpan = null, targetSpan = null) {
    const ray = this._createRay(a, b);
    const rayLength = Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);

    // Check if there are any walls at all
    const totalWalls = canvas?.walls?.objects?.children?.length || 0;
    if (totalWalls === 0) {
      return false;
    }

    if (attackerSpan && targetSpan) {
      const levelsIntegration = LevelsIntegration.getInstance();
      if (levelsIntegration.isActive) {
        const a3d = { x: a.x, y: a.y, z: (attackerSpan.bottom + attackerSpan.top) / 2 };
        const b3d = { x: b.x, y: b.y, z: (targetSpan.bottom + targetSpan.top) / 2 };
        if (!levelsIntegration.test3DPointCollision(a3d, b3d, 'sight')) {
          return false;
        }
      }
    }

    const walls = canvas.walls.objects?.children || [];
    for (const wall of walls) {
      const c = wall?.coords;
      if (!c) continue;

      // Check wall type and direction - walls that block sight should provide cover
      const wallDoc = wall.document || wall;

      // Skip walls that don't block sight from this direction
      if (!this._doesWallBlockFromDirection(wallDoc, a)) continue;

      // Check if the ray intersects this wall
      const intersection = this._lineIntersectionPoint(
        ray.A.x,
        ray.A.y,
        ray.B.x,
        ray.B.y,
        c[0],
        c[1],
        c[2],
        c[3],
      );
      if (intersection) {
        const intersectionDist = Math.sqrt(
          (intersection.x - a.x) ** 2 + (intersection.y - a.y) ** 2,
        );
        if (intersectionDist < rayLength - 1) {
          if (attackerSpan && targetSpan && rayLength > 0) {
            const t = intersectionDist / rayLength;
            if (!doesWallBlockLineOfSight(wallDoc, attackerSpan, targetSpan, t)) {
              continue;
            }
          } else if (elevationRange && !doesWallBlockAtElevation(wallDoc, elevationRange)) {
            continue;
          }
          return true;
        }
      }
    }

    // No walls blocked this ray
    return false;
  }

  /**
   * Filter blockers by elevation - check if blocker can intersect the 3D line of sight
   * @param {Object} attacker - Attacker token
   * @param {Object} target - Target token
   * @param {Array} blockers - Array of potential blocking tokens
   * @param {string} mode - Intersection mode to determine elevation calculation method
   * @returns {Array} Filtered array of blockers that can actually block the line of sight
   * @private
   */
  _filterBlockersByElevation(attacker, target, blockers, mode = 'any') {
    try {
      const attSpan = getTokenVerticalSpanFt(attacker);
      const tgtSpan = getTokenVerticalSpanFt(target);

      if (mode === 'tactical') {
        // Tactical mode: use corner-to-corner elevation calculations
        return this._filterBlockersByElevationTactical(
          attacker,
          target,
          blockers,
          attSpan,
          tgtSpan,
        );
      } else if (mode === 'any' || mode === 'length10') {
        // Any/10% modes: use permissive elevation filtering (horizontal intersection focus)
        return this._filterBlockersByElevationPermissive(
          attacker,
          target,
          blockers,
          attSpan,
          tgtSpan,
        );
      } else if (mode === 'coverage') {
        // Coverage mode: use moderate elevation filtering
        return this._filterBlockersByElevationModerate(
          attacker,
          target,
          blockers,
          attSpan,
          tgtSpan,
        );
      } else {
        // Default: use center-to-center elevation calculations
        return this._filterBlockersByElevationCenterToCenter(
          attacker,
          target,
          blockers,
          attSpan,
          tgtSpan,
        );
      }
    } catch {
      // If elevation filtering fails, return all blockers
      return blockers;
    }
  }

  /**
   * Filter blockers by elevation using center-to-center line of sight
   * @param {Object} attacker - Attacker token
   * @param {Object} target - Target token
   * @param {Array} blockers - Array of potential blocking tokens
   * @param {Object} attSpan - Attacker vertical span
   * @param {Object} tgtSpan - Target vertical span
   * @returns {Array} Filtered array of blockers
   * @private
   */
  _filterBlockersByElevationCenterToCenter(attacker, target, blockers, attSpan, tgtSpan) {
    // Get horizontal positions
    const attPos = attacker.center ?? attacker.getCenterPoint();
    const tgtPos = target.center ?? target.getCenterPoint();

    return blockers.filter((blocker) => {
      try {
        const blockerSpan = getTokenVerticalSpanFt(blocker);
        const blockerPos = blocker.center ?? blocker.getCenterPoint();

        // Check if blocker is horizontally between attacker and target
        // If not, it can't block regardless of elevation
        if (!this._isHorizontallyBetween(attPos, tgtPos, blockerPos)) {
          return false;
        }

        // Calculate the elevation of the line of sight at the blocker's horizontal position
        const lineOfSightElevationAtBlocker = this._calculateLineOfSightElevationAt(
          attPos,
          attSpan,
          tgtPos,
          tgtSpan,
          blockerPos,
        );

        // Check if the blocker's vertical span intersects with the line of sight elevation range
        return this._verticalSpansIntersect(blockerSpan, lineOfSightElevationAtBlocker);
      } catch {
        // If we can't determine elevation, include the blocker to be safe
        return true;
      }
    });
  }

  /**
   * Filter blockers by elevation using permissive logic (any/10% modes)
   * These modes focus on horizontal intersection, so we're more lenient with elevation
   * @param {Object} attacker - Attacker token
   * @param {Object} target - Target token
   * @param {Array} blockers - Array of potential blocking tokens
   * @param {Object} attSpan - Attacker vertical span
   * @param {Object} tgtSpan - Target vertical span
   * @returns {Array} Filtered array of blockers
   * @private
   */
  _filterBlockersByElevationPermissive(attacker, target, blockers, attSpan, tgtSpan) {
    // Get horizontal positions
    const attPos = attacker.center ?? attacker.getCenterPoint();
    const tgtPos = target.center ?? target.getCenterPoint();

    return blockers.filter((blocker) => {
      try {
        const blockerSpan = getTokenVerticalSpanFt(blocker);
        const blockerPos = blocker.center ?? blocker.getCenterPoint();

        // Check if blocker is horizontally between attacker and target
        if (!this._isHorizontallyBetween(attPos, tgtPos, blockerPos)) {
          return false;
        }

        // For any/10% modes, we use a very permissive elevation check
        // These modes focus on horizontal intersection, so we're very lenient with elevation
        // Only filter out blockers that are completely above or below all possible sight lines

        // Calculate the interpolation factor (how far along the line the blocker is)
        const totalDist = Math.sqrt((tgtPos.x - attPos.x) ** 2 + (tgtPos.y - attPos.y) ** 2);
        const blockerDist = Math.sqrt(
          (blockerPos.x - attPos.x) ** 2 + (blockerPos.y - attPos.y) ** 2,
        );
        const t = totalDist > 0 ? blockerDist / totalDist : 0;

        // Calculate the range of all possible sight lines at the blocker position
        const highestSightLine = attSpan.top + t * (tgtSpan.top - attSpan.top);
        const lowestSightLine = attSpan.bottom + t * (tgtSpan.bottom - attSpan.bottom);
        const sightLineRange = {
          bottom: Math.min(highestSightLine, lowestSightLine),
          top: Math.max(highestSightLine, lowestSightLine),
        };

        // Very permissive check: blocker provides cover if it has ANY overlap with the sight line range
        return blockerSpan.bottom < sightLineRange.top && blockerSpan.top > sightLineRange.bottom;
      } catch {
        // If we can't determine elevation, include the blocker to be safe
        return true;
      }
    });
  }

  /**
   * Filter blockers by elevation using moderate logic (coverage mode)
   * Coverage mode uses a balanced approach between strict and permissive
   * @param {Object} attacker - Attacker token
   * @param {Object} target - Target token
   * @param {Array} blockers - Array of potential blocking tokens
   * @param {Object} attSpan - Attacker vertical span
   * @param {Object} tgtSpan - Target vertical span
   * @returns {Array} Filtered array of blockers
   * @private
   */
  _filterBlockersByElevationModerate(attacker, target, blockers, attSpan, tgtSpan) {
    // Get horizontal positions
    const attPos = attacker.center ?? attacker.getCenterPoint();
    const tgtPos = target.center ?? target.getCenterPoint();

    return blockers.filter((blocker) => {
      try {
        const blockerSpan = getTokenVerticalSpanFt(blocker);
        const blockerPos = blocker.center ?? blocker.getCenterPoint();

        // Check if blocker is horizontally between attacker and target
        if (!this._isHorizontallyBetween(attPos, tgtPos, blockerPos)) {
          return false;
        }

        // For coverage mode, use center-to-center line but with more tolerance
        const lineOfSightElevationAtBlocker = this._calculateLineOfSightElevationAt(
          attPos,
          attSpan,
          tgtPos,
          tgtSpan,
          blockerPos,
        );

        // Use a larger tolerance for coverage mode (3ft instead of exact)
        const tolerance = 3; // 3 feet tolerance
        const adjustedRange = {
          bottom: lineOfSightElevationAtBlocker.bottom - tolerance,
          top: lineOfSightElevationAtBlocker.top + tolerance,
        };

        // Check if the blocker's vertical span intersects with the adjusted line of sight range
        return blockerSpan.bottom < adjustedRange.top && blockerSpan.top > adjustedRange.bottom;
      } catch {
        // If we can't determine elevation, include the blocker to be safe
        return true;
      }
    });
  }

  /**
   * Filter blockers by elevation using corner-to-corner line of sight (tactical mode)
   * @param {Object} attacker - Attacker token
   * @param {Object} target - Target token
   * @param {Array} blockers - Array of potential blocking tokens
   * @param {Object} attSpan - Attacker vertical span
   * @param {Object} tgtSpan - Target vertical span
   * @returns {Array} Filtered array of blockers
   * @private
   */
  _filterBlockersByElevationTactical(attacker, target, blockers, attSpan, tgtSpan) {
    // Get token rectangles and corners
    const attackerRect = getTokenRect(attacker);
    const targetRect = getTokenRect(target);
    const attackerSizeValue = attacker?.actor?.system?.traits?.size?.value ?? 'med';
    const targetSizeValue = target?.actor?.system?.traits?.size?.value ?? 'med';
    const attackerCorners = getTokenCorners(attacker, attackerRect, attackerSizeValue);
    const targetCorners = getTokenCorners(target, targetRect, targetSizeValue);

    return blockers.filter((blocker) => {
      try {
        const blockerSpan = getTokenVerticalSpanFt(blocker);
        const blockerPos = blocker.center ?? blocker.getCenterPoint();

        // Check if any corner-to-corner line could potentially intersect this blocker
        for (const attackerCorner of attackerCorners) {
          for (const targetCorner of targetCorners) {
            // Check if blocker is horizontally between these corners
            if (!this._isHorizontallyBetween(attackerCorner, targetCorner, blockerPos)) {
              continue;
            }

            // Calculate elevation of this corner-to-corner line at blocker position
            const lineOfSightElevation = this._calculateCornerToCornerElevationAt(
              attackerCorner,
              attSpan,
              targetCorner,
              tgtSpan,
              blockerPos,
            );

            // If this line intersects the blocker, include the blocker
            if (this._verticalSpansIntersect(blockerSpan, lineOfSightElevation)) {
              return true;
            }
          }
        }

        // No corner-to-corner line intersects this blocker
        return false;
      } catch {
        // If we can't determine elevation, include the blocker to be safe
        return true;
      }
    });
  }

  /**
   * Check if a point is horizontally between two other points (roughly on the line)
   * @param {Object} p1 - First point {x, y}
   * @param {Object} p2 - Second point {x, y}
   * @param {Object} test - Test point {x, y}
   * @returns {boolean} True if test point is roughly between p1 and p2
   * @private
   */
  _isHorizontallyBetween(p1, p2, test) {
    // Use a simple distance check - if the sum of distances from test to p1 and p2
    // is approximately equal to the distance from p1 to p2, then test is on the line
    const d1 = Math.sqrt((test.x - p1.x) ** 2 + (test.y - p1.y) ** 2);
    const d2 = Math.sqrt((test.x - p2.x) ** 2 + (test.y - p2.y) ** 2);
    const total = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);

    // Allow some tolerance for floating point precision and token size
    const tolerance = Math.max(50, total * 0.1); // 50 pixels or 10% of line length
    return Math.abs(d1 + d2 - total) <= tolerance;
  }

  /**
   * Calculate the elevation of a corner-to-corner line of sight at a specific horizontal position
   * @param {Object} attackerCorner - Attacker corner position {x, y}
   * @param {Object} attSpan - Attacker vertical span {bottom, top}
   * @param {Object} targetCorner - Target corner position {x, y}
   * @param {Object} tgtSpan - Target vertical span {bottom, top}
   * @param {Object} blockerPos - Blocker position {x, y}
   * @returns {Object} Elevation range {bottom, top} at the blocker position
   * @private
   */
  _calculateCornerToCornerElevationAt(attackerCorner, attSpan, targetCorner, tgtSpan, blockerPos) {
    // Calculate the interpolation factor (how far along the line the blocker is)
    const totalDist = Math.sqrt(
      (targetCorner.x - attackerCorner.x) ** 2 + (targetCorner.y - attackerCorner.y) ** 2,
    );
    const blockerDist = Math.sqrt(
      (blockerPos.x - attackerCorner.x) ** 2 + (blockerPos.y - attackerCorner.y) ** 2,
    );
    const t = totalDist > 0 ? blockerDist / totalDist : 0;

    // For corner-to-corner, we use the center elevations of the tokens
    // (corners are horizontal positions, but elevation is still based on token center)
    const attackerCenterElevation = (attSpan.bottom + attSpan.top) / 2;
    const targetCenterElevation = (tgtSpan.bottom + tgtSpan.top) / 2;

    // Interpolate the corner-to-corner line of sight elevation at the blocker position
    const lineOfSightElevation =
      attackerCenterElevation + t * (targetCenterElevation - attackerCenterElevation);

    // Small tolerance for practical implementation
    const tolerance = 1; // 1 foot tolerance

    return {
      bottom: lineOfSightElevation - tolerance,
      top: lineOfSightElevation + tolerance,
    };
  }

  /**
   * Calculate the elevation of the center-to-center line of sight at a specific horizontal position
   * PF2E uses center-to-center line of sight for cover calculations
   * @param {Object} attPos - Attacker position {x, y}
   * @param {Object} attSpan - Attacker vertical span {bottom, top}
   * @param {Object} tgtPos - Target position {x, y}
   * @param {Object} tgtSpan - Target vertical span {bottom, top}
   * @param {Object} blockerPos - Blocker position {x, y}
   * @returns {Object} Elevation range {bottom, top} at the blocker position
   * @private
   */
  _calculateLineOfSightElevationAt(attPos, attSpan, tgtPos, tgtSpan, blockerPos) {
    // Calculate the interpolation factor (how far along the line the blocker is)
    const totalDist = Math.sqrt((tgtPos.x - attPos.x) ** 2 + (tgtPos.y - attPos.y) ** 2);
    const blockerDist = Math.sqrt((blockerPos.x - attPos.x) ** 2 + (blockerPos.y - attPos.y) ** 2);
    const t = totalDist > 0 ? blockerDist / totalDist : 0;

    // PF2E uses center-to-center line of sight
    // Calculate the center elevations of attacker and target
    const attackerCenterElevation = (attSpan.bottom + attSpan.top) / 2;
    const targetCenterElevation = (tgtSpan.bottom + tgtSpan.top) / 2;

    // Interpolate the center-to-center line of sight elevation at the blocker position
    const lineOfSightElevation =
      attackerCenterElevation + t * (targetCenterElevation - attackerCenterElevation);

    // For cover purposes, we need to consider the blocker's height
    // A blocker provides cover if the line of sight passes through its vertical space
    // Use the blocker's height as tolerance rather than arbitrary 1ft
    return {
      bottom: lineOfSightElevation,
      top: lineOfSightElevation,
    };
  }

  /**
   * Check if a blocker can provide cover by blocking the center-to-center line of sight
   * @param {Object} blockerSpan - Blocker's vertical span {bottom, top}
   * @param {Object} lineOfSightRange - Line of sight elevation {bottom, top} (same value for center-to-center)
   * @returns {boolean} True if blocker can provide cover
   * @private
   */
  _verticalSpansIntersect(blockerSpan, lineOfSightRange) {
    // In PF2E, a blocker provides cover if the center-to-center line of sight passes through its vertical space
    // The line of sight is at a specific elevation, check if it's within the blocker's height
    const lineOfSightElevation = lineOfSightRange.bottom; // Same as .top for center-to-center
    return lineOfSightElevation >= blockerSpan.bottom && lineOfSightElevation <= blockerSpan.top;
  }

  /**
   * Get eligible blocking tokens
   * @param {Object} attacker
   * @param {Object} target
   * @param {Object} filters
   * @returns {Array}
   * @private
   */
  _getEligibleBlockingTokens(attacker, target, filters) {
    const out = [];
    const log = getLogger('AutoCover');

    for (const blocker of canvas.tokens.placeables) {
      if (!blocker?.actor) continue;
      if (blocker === attacker || blocker === target) continue;

      // Exclude controlled/selected tokens from being blockers
      if (
        canvas.tokens.controlled.includes(blocker) ||
        blocker.id === attacker.id ||
        blocker.id === target.id
      )
        continue;

      const type = blocker.actor?.type;
      if (type === 'loot' || type === 'hazard') continue;
      // Token cover overrides are handled later in _applyTokenCoverOverrides
      // Don't filter out tokens here based on cover override
      // Always ignore Foundry hidden tokens
      if (blocker.document.hidden) {
        continue;
      }

      // Check PF2e undetected tokens only if the setting is enabled
      if (filters.ignoreUndetected) {
        try {
          // Use custom visibility perspective if provided, otherwise use attacker
          const perspectiveToken = filters.visibilityPerspective || attacker;
          const vis = getVisibilityBetween(perspectiveToken, blocker);
          if (vis === 'undetected') {
            continue;
          }
        } catch { }
      }
      if (filters.ignoreDead && blocker.actor?.hitPoints?.value === 0) {
        continue;
      }
      if (!filters.allowProneBlockers) {
        try {
          const itemConditions = blocker.actor?.itemTypes?.condition || [];
          const legacyConditions =
            blocker.actor?.conditions?.conditions || blocker.actor?.conditions || [];
          const isProne =
            itemConditions.some((c) => c?.slug === 'prone') ||
            legacyConditions.some((c) => c?.slug === 'prone');
          if (isProne) {
            continue;
          }
        } catch { }
      }
      if (filters.ignoreAllies && blocker.actor?.alliance === filters.attackerAlliance) {
        continue;
      }
      if (filters.ignoreSmallerTokens || filters.ignoreSameSizeTokens || filters.ignoreLargerTokens) {
        const blockerRank = getSizeRank(blocker);
        const attackerRank = filters.attackerSizeRank;
        if (
          (blockerRank < attackerRank && filters.ignoreSmallerTokens) ||
          (blockerRank === attackerRank && filters.ignoreSameSizeTokens) ||
          (blockerRank > attackerRank && filters.ignoreLargerTokens)
        ) {
          continue;
        }
      }

      // Check size-based cover rules
      if (!this._canTokenProvideCover(attacker, target, blocker)) {
        continue;
      }

      // Check rule elements: if blocker has a rule element preventing it from providing cover to target
      const attackContext = filters.attackContext || null;
      const ruleElementCheck = RuleElementCoverService.canTokenProvideCoverToTarget(blocker, target, attackContext);
      if (!ruleElementCheck.allowed) {
        // Track this for chat indicators (avoid duplicates)
        const key = `${attacker.id}:${target.id}`;
        if (!this._ruleElementBlocks.has(key)) {
          this._ruleElementBlocks.set(key, []);
        }
        const blocks = this._ruleElementBlocks.get(key);
        // Only add if we haven't already tracked this blocker
        if (!blocks.some(re => re.tokenId === blocker.id)) {
          this._ruleElementBlocks.get(key).push(ruleElementCheck.ruleElement);
        }
        continue;
      }

      out.push(blocker);
    }

    return out;
  }

  _shouldIgnoreBlockerForSnipingDuo(attacker, blocker, attackContext = null) {
    try {
      if (!attacker?.actor || !blocker?.actor) return false;

      // Only apply to Strikes
      const strikeOk = !attackContext || this._isStrikeAttackContext(attackContext);
      if (!strikeOk) {
        const log = getLogger('AutoCover');
        const attackerActor = attacker.actor;
        const blockerActor = blocker.actor;
        const attackerKey = this._getActorKey(attackerActor);
        const blockerKey = this._getActorKey(blockerActor);
        const spotterKey = FeatsHandler.hasFeat(attacker, 'sniping-duo-dedication')
          ? this._readActorFlag(attackerActor, 'snipingDuoSpotterActorUuid')
          : null;
        const sniperKey = this._readActorFlag(attackerActor, 'snipingDuoSniperActorUuid');
        const nBlocker = this._normalizeActorKey(blockerKey);
        const nSpotter = this._normalizeActorKey(spotterKey);
        const nSniper = this._normalizeActorKey(sniperKey);
        const candidate = !!(
          nBlocker?.id &&
          (nBlocker.id === nSpotter?.id || nBlocker.id === nSniper?.id)
        );

        if (candidate) {
          log.debug('Sniping Duo: strike context not detected; not ignoring blocker', () => ({
            attackerName: attacker?.name,
            attackerActorKey: attackerKey,
            blockerName: blocker?.name,
            blockerActorKey: blockerKey,
            spotterKey,
            sniperKey,
            attackContextType: attackContext?.type ?? null,
            attackContextOptions: this._formatRollOptionsForLog(attackContext?.options),
            attackContextDomains: Array.isArray(attackContext?.domains)
              ? attackContext.domains.slice(0, 20)
              : null,
            itemName: attackContext?.item?.name ?? null,
          }));
        }
      }
      if (attackContext && !strikeOk) return false;

      const attackerActor = attacker.actor;
      const blockerActor = blocker.actor;
      const attackerKey = this._getActorKey(attackerActor);
      const blockerKey = this._getActorKey(blockerActor);
      if (!attackerKey || !blockerKey) return false;

      // Case A: attacker is the sniper (has dedication) and blocker is the designated spotter
      const attackerHasDedication = FeatsHandler.hasFeat(attacker, 'sniping-duo-dedication');
      const spotterKey = attackerHasDedication
        ? this._readActorFlag(attackerActor, 'snipingDuoSpotterActorUuid')
        : null;
      if (attackerHasDedication) {
        if (spotterKey && this._actorKeysMatch(spotterKey, blockerKey)) return true;
      }

      // Case B: attacker is the spotter. Ignore cover from the sniper if there's a verified link.
      const sniperKey = this._readActorFlag(attackerActor, 'snipingDuoSniperActorUuid');
      if (!sniperKey || !this._actorKeysMatch(sniperKey, blockerKey)) return false;

      const sniperToken = this._findTokenByActorKey(sniperKey);
      if (!sniperToken?.actor) return false;
      if (!FeatsHandler.hasFeat(sniperToken, 'sniping-duo-dedication')) return false;

      const sniperSpotterKey = this._readActorFlag(sniperToken.actor, 'snipingDuoSpotterActorUuid');
      return !!(sniperSpotterKey && this._actorKeysMatch(sniperSpotterKey, attackerKey));
    } catch {
      return false;
    }
  }

  _recordSnipingDuoIgnoredBlocker(attacker, target, blocker) {
    const log = getLogger('AutoCover');
    try {
      const aId = attacker?.id;
      const tId = target?.id;
      if (!aId || !tId) return;
      const key = `${aId}:${tId}`;
      if (!this._snipingDuoRecords.has(key)) {
        this._snipingDuoRecords.set(key, {
          feat: 'sniping-duo-dedication',
          ignoredTokenId: blocker?.id ?? null,
          ignoredTokenName: blocker?.name ?? null,
          ts: Date.now(),
        });
      }
    } catch { }

    log.debug('Sniping Duo: ignored blocker', () => ({
      attackerTokenId: attacker?.id,
      attackerName: attacker?.name,
      attackerActorKey: this._getActorKey(attacker?.actor),
      blockerTokenId: blocker?.id,
      blockerName: blocker?.name,
      blockerActorKey: this._getActorKey(blocker?.actor),
    }));
  }

  _formatRollOptionsForLog(rollOptions) {
    try {
      const arr = Array.isArray(rollOptions)
        ? rollOptions
        : rollOptions instanceof Set
          ? Array.from(rollOptions)
          : [];
      return arr.slice(0, 50);
    } catch {
      return null;
    }
  }

  _isStrikeAttackContext(attackContext) {
    try {
      if (!attackContext) return false;
      if (attackContext.type === 'strike-attack-roll') return true;

      const rollOptions = attackContext?.options;
      const optionsArray = Array.isArray(rollOptions)
        ? rollOptions
        : rollOptions instanceof Set
          ? Array.from(rollOptions)
          : [];

      if (optionsArray.includes('action:strike')) return true;

      return false;
    } catch {
      return false;
    }
  }

  _isRangedAttackContext(attackContext) {
    try {
      const rollOptions = attackContext?.options;
      if (rollOptions) {
        const optionsArray = Array.isArray(rollOptions)
          ? rollOptions
          : rollOptions instanceof Set
            ? Array.from(rollOptions)
            : [];

        const hasAnyExact = (opts) => opts.some((o) => optionsArray.includes(o));
        if (
          hasAnyExact([
            'attack:ranged',
            'attack:trait:ranged',
            'item:ranged',
            'item:trait:ranged',
            'ranged',
          ])
        ) {
          return true;
        }

        // Best-effort: some contexts encode ranged-ness in a longer option string.
        if (optionsArray.some((o) => typeof o === 'string' && (o.startsWith('attack:ranged') || o.endsWith(':ranged')))) {
          return true;
        }
      }

      const item = attackContext?.item;
      if (item) {
        if (item?.isRanged === true) return true;
        if (item?.isMelee === true) return false;

        const category = item.system?.category;
        if (
          category === 'simple-ranged' ||
          category === 'martial-ranged' ||
          category === 'advanced-ranged' ||
          category === 'unarmed-ranged'
        ) {
          return true;
        }

        const traits = item.system?.traits?.value || [];
        if (Array.isArray(traits) && traits.includes('ranged')) return true;

        const rangeRaw = item.system?.range ?? null;
        const range =
          (rangeRaw && typeof rangeRaw === 'object' ? (rangeRaw.value ?? rangeRaw.increment ?? null) : rangeRaw) ??
          item.system?.rangeIncrement ??
          null;
        if (range != null && Number(range) > 0) return true;
      }
    } catch {
      return false;
    }
    return false;
  }

  _readActorFlag(actor, key) {
    try {
      return actor?.getFlag?.(MODULE_ID, key) ?? actor?.flags?.[MODULE_ID]?.[key] ?? null;
    } catch {
      return null;
    }
  }

  _getActorKey(actor) {
    try {
      return actor?.uuid || actor?.id || null;
    } catch {
      return null;
    }
  }

  _actorKeysMatch(a, b) {
    try {
      const na = this._normalizeActorKey(a);
      const nb = this._normalizeActorKey(b);
      return !!(na && nb && (na.raw === nb.raw || na.id === nb.id));
    } catch {
      return false;
    }
  }

  _normalizeActorKey(key) {
    try {
      const raw = String(key || '').trim();
      if (!raw) return null;
      const parts = raw.split('.');
      const id = parts[parts.length - 1] || raw;
      return { raw, id };
    } catch {
      return null;
    }
  }

  _findTokenByActorKey(actorKey) {
    try {
      const key = this._normalizeActorKey(actorKey);
      if (!key) return null;
      const tokens = canvas?.tokens?.placeables || [];
      for (const t of tokens) {
        const a = t?.actor;
        if (!a) continue;
        const tKey = this._getActorKey(a);
        if (tKey && this._actorKeysMatch(key.raw, tKey)) return t;
      }
      return null;
    } catch {
      return null;
    }
  }

  /**
   * Check if a blocker token can provide cover based on PF2E cover rules
   * @param {Object} attacker - Attacker token
   * @param {Object} target - Target token
   * @param {Object} blocker - Potential blocker token
   * @returns {boolean} True if blocker can provide cover
   * @private
   */
  _canTokenProvideCover(attacker, target, blocker) {
    try {
      // Rule 1: Tokens in the same square as attacker or target cannot provide cover
      if (
        this._tokensInSameSquare(attacker, blocker) ||
        this._tokensInSameSquare(target, blocker)
      ) {
        return false;
      }

      // Rule 2: Get sizes for cover rules
      const targetSize = this._getTokenSizeCategory(target);
      const blockerSize = this._getTokenSizeCategory(blocker);

      // Rule 3: Tiny tokens cannot provide cover to non-tiny creatures
      if (blockerSize === 'tiny' && targetSize !== 'tiny') {
        return false;
      }

      // Rule 4: Additional size-based rules from the PF2E cover table
      // Tiny targets can only get cover from Small+ blockers (already covered by rule 3)
      // Small+ targets cannot get cover from tiny blockers (already covered by rule 3)

      return true;
    } catch {
      // If we can't determine sizes/positions, allow cover to be safe
      return true;
    }
  }

  /**
   * Check if two tokens are in the same grid square
   * @param {Object} token1 - First token
   * @param {Object} token2 - Second token
   * @returns {boolean} True if tokens occupy the same grid square
   * @private
   */
  _tokensInSameSquare(token1, token2) {
    try {
      // Get grid positions (top-left corner of tokens in grid units)
      const gridSize = canvas?.grid?.size || 50;

      const token1GridX = Math.floor(token1.document.x / gridSize);
      const token1GridY = Math.floor(token1.document.y / gridSize);
      const token2GridX = Math.floor(token2.document.x / gridSize);
      const token2GridY = Math.floor(token2.document.y / gridSize);

      // For tokens larger than 1x1, check if their grid areas overlap
      const token1Width = token1.document.width || 1;
      const token1Height = token1.document.height || 1;
      const token2Width = token2.document.width || 1;
      const token2Height = token2.document.height || 1;

      // Check for overlap in both X and Y dimensions
      const xOverlap =
        token1GridX < token2GridX + token2Width && token1GridX + token1Width > token2GridX;
      const yOverlap =
        token1GridY < token2GridY + token2Height && token1GridY + token1Height > token2GridY;

      return xOverlap && yOverlap;
    } catch {
      return false;
    }
  }

  /**
   * Get the size category of a token for cover calculations
   * @param {Object} token - Token object
   * @returns {string} Size category ('tiny', 'small', 'medium', 'large', 'huge', 'gargantuan')
   * @private
   */
  _getTokenSizeCategory(token) {
    try {
      const size = token?.actor?.system?.traits?.size?.value;
      if (!size) return 'medium'; // Default to medium if unknown

      // Normalize size values
      const sizeMap = {
        tiny: 'tiny',
        sm: 'small',
        small: 'small',
        med: 'medium',
        medium: 'medium',
        lg: 'large',
        large: 'large',
        huge: 'huge',
        grg: 'gargantuan',
        gargantuan: 'gargantuan',
      };

      return sizeMap[size] || 'medium';
    } catch {
      return 'medium';
    }
  }

  /**
   * Helper method to create a ray object
   * @param {Object} p1 - Start point {x, y}
   * @param {Object} p2 - End point {x, y}
   * @returns {Object} - A ray-like object with A, B, and distance properties
   * @private
   */
  _createRay(p1, p2) {
    // Use Foundry V13+ namespaced Ray class if available
    if (foundry?.canvas?.geometry?.Ray) {
      return new foundry.canvas.geometry.Ray(p1, p2);
    }
    // Fallback to global Ray if available (pre-V13)
    else if (typeof globalThis.Ray !== 'undefined') {
      return new globalThis.Ray(p1, p2);
    }

    // Otherwise, create a simple ray-like object
    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    const distance = Math.sqrt(dx * dx + dy * dy);

    return {
      A: { x: p1.x, y: p1.y },
      B: { x: p2.x, y: p2.y },
      distance: distance,
    };
  }

  /**
   * Evaluate cover provided by the creature's size
   * @param {Object} attacker - Attacker token
   * @param {Object} target - Target token
   * @param {Array} blockers - Array of potential blocking tokens
   * @returns {string} Cover state ('none', 'lesser', 'standard', 'greater')
   * @private
   */
  _evaluateCreatureSizeCover(attacker, target, blockers, attackContext = null) {
    try {
      if (!attacker || !target) return 'none';

      let any = false;
      let standard = false;
      const attackerSize = getSizeRank(attacker);
      const targetSize = getSizeRank(target);

      for (const blocker of blockers) {
        // Skip if blocker is the same as attacker or target
        if (blocker.id === attacker.id || blocker.id === target.id) continue;

        const rect = getTokenRect(blocker);

        // Create rectangle in the format expected by intersectsBetweenTokens
        const rectForIntersection = {
          x1: rect.x1,
          y1: rect.y1,
          x2: rect.x2,
          y2: rect.y2,
        };

        // Check if blocker intersects between tokens
        if (
          !intersectsBetweenTokens(
            attacker,
            target,
            rectForIntersection,
            this._getIntersectionMode(),
            blocker,
          )
        )
          continue;

        if (this._shouldIgnoreBlockerForSnipingDuo(attacker, blocker, attackContext)) {
          this._recordSnipingDuoIgnoredBlocker(attacker, target, blocker);
          continue;
        }

        any = true;
        const blockerSize = getSizeRank(blocker);
        const sizeDiffAttacker = blockerSize - attackerSize;
        const sizeDiffTarget = blockerSize - targetSize;
        const grantsStandard = sizeDiffAttacker >= 2 && sizeDiffTarget >= 2;

        if (grantsStandard) standard = true;
      }

      const result = any ? (standard ? 'standard' : 'lesser') : 'none';
      let finalState = result;

      const hasBlockerWithOverride = blockers.some((blocker) => {
        const override = blocker.document?.getFlag?.(MODULE_ID, 'coverOverride');
        return override && override !== 'auto';
      });

      try {
        // Ceaseless Shadows: the TARGET (being attacked) has the feat and gets upgraded cover
        const upgraded = FeatsHandler.upgradeCoverForCreature(target, result)?.state || result;
        if (upgraded !== result) {
          try {
            const aId = attacker?.id;
            const tId = target?.id;
            if (aId && tId) {
              this._featUpgradeRecords.set(`${aId}:${tId}`, {
                from: result,
                to: upgraded,
                feat: 'ceaseless-shadows',
                ts: Date.now(),
                hasBlockerWithOverride,
              });
            }
          } catch { }
        }
        finalState = upgraded;
      } catch { }
      return finalState;
    } catch (error) {
      console.error('PF2E Visioner | Error in evaluateCreatureSizeCover:', error);
      return 'none';
    }
  }

  consumeFeatCoverUpgrade(attackerId, targetId) {
    try {
      const key = `${attackerId}:${targetId}`;
      if (!this._featUpgradeRecords.has(key)) return null;
      const rec = this._featUpgradeRecords.get(key);
      this._featUpgradeRecords.delete(key);
      if (Date.now() - rec.ts > 15000) return null;
      return rec;
    } catch {
      return null;
    }
  }

  consumeRuleElementBlocks(attackerId, targetId) {
    try {
      const key = `${attackerId}:${targetId}`;
      if (!this._ruleElementBlocks.has(key)) return null;
      const blocks = this._ruleElementBlocks.get(key);
      this._ruleElementBlocks.delete(key);
      return blocks.length > 0 ? blocks : null;
    } catch { return null; }
  }

  consumeSnipingDuoCoverIgnore(attackerId, targetId) {
    try {
      const key = `${attackerId}:${targetId}`;
      if (!this._snipingDuoRecords.has(key)) return null;
      const rec = this._snipingDuoRecords.get(key);
      this._snipingDuoRecords.delete(key);
      if (!rec?.ts || Date.now() - rec.ts > 15000) return null;
      return rec;
    } catch {
      return null;
    }
  }

  peekSnipingDuoCoverIgnore(attackerId, targetId) {
    try {
      const key = `${attackerId}:${targetId}`;
      if (!this._snipingDuoRecords.has(key)) return null;
      const rec = this._snipingDuoRecords.get(key);
      if (!rec?.ts || Date.now() - rec.ts > 15000) return null;
      return rec;
    } catch {
      return null;
    }
  }

  // Using segmentIntersectsRect from geometry-utils.js instead of _rayIntersectRect

  /**
   * Evaluate cover by tactical rules
   * @param {Object} attacker
   * @param {Object} target
   * @param {Array} blockers
   * @param {Object} elevationRange - Optional elevation range {bottom, top} for wall height filtering
   * @param {Object|null} attackContext - Optional PF2e roll context
   * @returns {string}
   * @private
   */
  _evaluateCoverByTactical(attacker, target, blockers, elevationRange = null, attackContext = null, attackerSpan = null, targetSpan = null) {
    // Tactical mode: corner-to-corner calculations
    // Choose the best corner of the attacker and check lines from all target corners to that corner
    // This matches the "choose a corner" tactical rule

    const attackerRect = getTokenRect(attacker);
    const targetRect = getTokenRect(target);

    // Debug token sizes and rectangles
    const attackerSizeValue = attacker?.actor?.system?.traits?.size?.value ?? 'med';
    const targetSizeValue = target?.actor?.system?.traits?.size?.value ?? 'med';

    const attackerCorners = getTokenCorners(attacker, attackerRect, attackerSizeValue);
    const targetCorners = getTokenCorners(target, targetRect, targetSizeValue);

    let bestCover = 'greater'; // Start with worst case

    // Try each attacker corner and find the one with the least cover (best for attacking)
    for (let a = 0; a < attackerCorners.length; a++) {
      const attackerCorner = attackerCorners[a];
      let blockedLines = 0;
      let wallBlockedAny = false;

      // Check lines from all target corners to this attacker corner
      for (let t = 0; t < targetCorners.length; t++) {
        const targetCorner = targetCorners[t];
        let lineBlocked = false;

        // Check if this line is blocked by walls
        if (this._isRayBlockedByWalls(targetCorner, attackerCorner, elevationRange, attackerSpan, targetSpan)) {
          lineBlocked = true;
          wallBlockedAny = true;
        }

        // Check if this line is blocked by any token blockers
        if (!lineBlocked) {
          for (const blocker of blockers) {
            if (blocker === attacker || blocker === target) continue;

            const blockerRect = getTokenRect(blocker);
            const intersectionLength = segmentRectIntersectionLength(
              targetCorner,
              attackerCorner,
              blockerRect,
            );
            if (intersectionLength > 0) {
              if (this._shouldIgnoreBlockerForSnipingDuo(attacker, blocker, attackContext)) {
                this._recordSnipingDuoIgnoredBlocker(attacker, target, blocker);
                continue;
              }
              lineBlocked = true;
              break;
            }
          }
        }

        if (lineBlocked) blockedLines++;
      }

      // Determine cover level for this attacker corner
      // Walls never grant lesser cover - only standard or greater
      let coverForThisCorner;
      if (blockedLines === 0) coverForThisCorner = 'none';
      else if (blockedLines === 1) coverForThisCorner = wallBlockedAny ? 'standard' : 'lesser';
      else if (blockedLines <= 3) coverForThisCorner = 'standard';
      else coverForThisCorner = 'greater';

      // Keep the best (lowest) cover result
      const coverOrder = ['none', 'lesser', 'standard', 'greater'];
      if (coverOrder.indexOf(coverForThisCorner) < coverOrder.indexOf(bestCover)) {
        bestCover = coverForThisCorner;
      }
    }

    // Return the best (lowest) cover across attacker corners
    return bestCover;
  }

  _evaluateCoverByCoverage(attacker, target, blockers, attackContext = null) {
    try {
      // If no blockers, no cover
      if (!blockers.length) return 'none';

      // Get centers
      const p1 = attacker.center ?? attacker.getCenterPoint();
      const p2 = target.center ?? target.getCenterPoint();

      const coverOrder = ['none', 'lesser', 'standard', 'greater'];
      let bestCover = 'none';

      for (const blocker of blockers) {
        const perBlocker = this._calculateCoverageByBlocker(p1, p2, [blocker]);
        if (!perBlocker || perBlocker === 'none') continue;

        if (this._shouldIgnoreBlockerForSnipingDuo(attacker, blocker, attackContext)) {
          this._recordSnipingDuoIgnoredBlocker(attacker, target, blocker);
          continue;
        }

        if (coverOrder.indexOf(perBlocker) > coverOrder.indexOf(bestCover)) {
          bestCover = perBlocker;
        }
      }

      return bestCover;
    } catch (error) {
      console.error('PF2E Visioner | Error in evaluateCoverByCoverage:', error);
      return 'none';
    }
  }

  /**
   * 2D line segment intersection that returns the intersection point.
   * @param {number} x1, y1, x2, y2 - First line segment coordinates
   * @param {number} x3, y3, x4, y4 - Second line segment coordinates
   * @returns {Object|null} Intersection point {x, y} or null if no intersection
   */
  _lineIntersectionPoint(x1, y1, x2, y2, x3, y3, x4, y4) {
    const d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
    if (d === 0) return null; // Lines are parallel

    const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / d;
    const u = -(((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / d);

    if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
      // Calculate intersection point
      const x = x1 + t * (x2 - x1);
      const y = y1 + t * (y2 - y1);
      return { x, y };
    }

    return null; // No intersection within segments
  }

  /**
   * Calculate coverage percentage by a single blocker
   * @param {Object} blocker - The blocker token
   * @param {Ray} ray - The ray from attacker to target
   * @param {number} rayLength - The length of the ray
   * @returns {number} - Coverage percentage (0-100)
   * @private
   */
  _calculateCoverageByBlocker(p1, p2, blockers) {
    // Fixed side coverage thresholds: Standard at 50%, Greater at 70%
    const lesserT = 50;
    const greaterT = 70;

    let sawAny = false;
    let meetsStd = false;
    let meetsGrt = false;
    for (const b of blockers) {
      const rect = getTokenRect(b);
      const len = segmentRectIntersectionLength(p1, p2, rect);
      if (len <= 0) continue;
      sawAny = true;
      const width = Math.abs(rect.x2 - rect.x1);
      const height = Math.abs(rect.y2 - rect.y1);
      const side = Math.max(width, height); // larger side in pixels
      const f = (len / Math.max(1, side)) * 100; // percent side coverage
      if (f >= greaterT) {
        meetsGrt = true;
        break;
      }
      if (f >= lesserT) {
        meetsStd = true;
      }
    }

    const result = meetsGrt ? 'greater' : meetsStd ? 'standard' : sawAny ? 'lesser' : 'none';
    return result;
  }

  /**
   * Apply token cover overrides as direct replacements to the calculated cover
   * @param {Object} attacker - Attacking token
   * @param {Object} target - Target token
   * @param {Array} blockers - Array of blocking tokens
   * @param {string} calculatedCover - The cover calculated by normal rules
   * @returns {string} Final cover after applying overrides
   * @private
   */
  _applyTokenCoverOverrides(attacker, target, blockers, calculatedCover) {
    try {
      if (!blockers.length) {
        return calculatedCover;
      }

      // Check each blocker for cover overrides
      for (const blocker of blockers) {
        const tokenCoverOverride = blocker.document?.getFlag?.(MODULE_ID, 'coverOverride');

        // If this token has an override, return it directly
        if (tokenCoverOverride && tokenCoverOverride !== 'auto') {
          return tokenCoverOverride;
        }
      }

      // No overrides found, use calculated cover
      return calculatedCover;
    } catch {
      return calculatedCover;
    }
  }

  /**
   * Check if target has cover from creatures larger than it between observer and target point.
   * Used by Distracting Shadows feat: creatures at least one size larger can provide cover
   * for Hide/Sneak prerequisite checks.
   *
   * @param {Token} observer - The observing token
   * @param {Token} target - The target token (actor using feat)
   * @param {Object} targetPoint - Optional specific point instead of target center {x, y}
   * @returns {boolean} True if at least one qualifying larger creature blocks the line
   */
  hasLargeCreatureCover(observer, target, targetPoint = null) {
    try {
      if (!observer || !target) return false;

      const p1 = observer?.center || observer?.getCenterPoint?.();
      let p2 = null;
      if (targetPoint && typeof targetPoint.x === 'number' && typeof targetPoint.y === 'number') {
        p2 = { x: targetPoint.x, y: targetPoint.y };
      } else {
        p2 = target?.center || target?.getCenterPoint?.();
      }
      if (!p1 || !p2) return false;

      const targetRank = getSizeRank(target);
      const tokens = canvas?.tokens?.placeables || [];

      for (const blocker of tokens) {
        if (!blocker?.actor) continue;
        if (blocker.id === target.id || blocker.id === observer.id) continue;
        if (blocker.document?.hidden) continue;

        const type = blocker.actor?.type;
        if (type === 'loot' || type === 'hazard') continue;

        const blockerRank = getSizeRank(blocker);
        if (!(blockerRank >= targetRank + 1)) continue;

        const rect = getTokenRect(blocker);
        const len = segmentRectIntersectionLength(p1, p2, rect);
        if (len > 0) {
          return true;
        }
      }
      return false;
    } catch (e) {
      console.warn('PF2E Visioner | CoverDetector.hasLargeCreatureCover failed:', e);
      return false;
    }
  }

  _checkRuleElementCover(attacker, target) {
    return RuleElementCoverService.getCoverFromRuleElements(attacker, target);
  }
}

const coverDetector = new CoverDetector();
export default coverDetector;
