import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import { logger } from "../../core/monitoring/logger.js";
import { LinearClient } from "./client.js";
class LinearSyncEngine {
  taskStore;
  linearClient;
  authManager;
  config;
  mappings = /* @__PURE__ */ new Map();
  projectRoot;
  mappingsPath;
  constructor(taskStore, authManager, config, projectRoot) {
    this.taskStore = taskStore;
    this.authManager = authManager;
    this.config = config;
    this.projectRoot = projectRoot || process.cwd();
    this.mappingsPath = join(
      this.projectRoot,
      ".stackmemory",
      "linear-mappings.json"
    );
    const apiKey = process.env["LINEAR_API_KEY"];
    if (apiKey) {
      this.linearClient = new LinearClient({
        apiKey
      });
    } else {
      const tokens = this.authManager.loadTokens();
      if (!tokens) {
        throw new Error(
          'Linear API key or authentication tokens not found. Set LINEAR_API_KEY environment variable or run "stackmemory linear setup" first.'
        );
      }
      this.linearClient = new LinearClient({
        apiKey: tokens.accessToken,
        useBearer: true,
        onUnauthorized: async () => {
          const refreshed = await this.authManager.refreshAccessToken();
          return refreshed.accessToken;
        }
      });
    }
    this.loadMappings();
  }
  /**
   * Update sync configuration
   */
  updateConfig(newConfig) {
    this.config = { ...this.config, ...newConfig };
  }
  /**
   * Perform bi-directional sync
   */
  async sync() {
    if (!this.config.enabled) {
      return {
        success: false,
        synced: { toLinear: 0, fromLinear: 0, updated: 0 },
        conflicts: [],
        errors: ["Sync is disabled"]
      };
    }
    const result = {
      success: true,
      synced: { toLinear: 0, fromLinear: 0, updated: 0 },
      conflicts: [],
      errors: []
    };
    try {
      const apiKey = process.env["LINEAR_API_KEY"];
      if (!apiKey) {
        const token = await this.authManager.getValidToken();
        this.linearClient = new LinearClient({
          apiKey: token,
          useBearer: true,
          onUnauthorized: async () => {
            const refreshed = await this.authManager.refreshAccessToken();
            return refreshed.accessToken;
          }
        });
      }
      if (!this.config.defaultTeamId) {
        const team = await this.linearClient.getTeam();
        this.config.defaultTeamId = team.id;
        logger.info(`Using Linear team: ${team.name} (${team.key})`);
      }
      if (this.config.direction === "bidirectional" || this.config.direction === "to_linear") {
        const toLinearResult = await this.syncToLinear();
        result.synced.toLinear = toLinearResult.created;
        result.synced.updated += toLinearResult.updated;
        result.errors.push(...toLinearResult.errors);
      }
      if (this.config.direction === "bidirectional" || this.config.direction === "from_linear") {
        const fromLinearResult = await this.syncFromLinear();
        result.synced.fromLinear = fromLinearResult.created;
        result.synced.updated += fromLinearResult.updated;
        result.conflicts.push(...fromLinearResult.conflicts);
        result.errors.push(...fromLinearResult.errors);
      }
      this.saveMappings();
    } catch (error) {
      result.success = false;
      result.errors.push(`Sync failed: ${String(error)}`);
      logger.error("Linear sync failed:", error);
    }
    return result;
  }
  /**
   * Delay helper for rate limiting
   */
  async delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
  /**
   * Sync tasks from StackMemory to Linear
   */
  async syncToLinear() {
    const result = { created: 0, updated: 0, errors: [] };
    const maxBatchSize = this.config.maxBatchSize || 10;
    const rateLimitDelay = this.config.rateLimitDelay || 500;
    const duplicateDetector = new LinearDuplicateDetector(this.linearClient);
    const unsyncedTasks = this.getUnsyncedTasks();
    const tasksToSync = unsyncedTasks.slice(0, maxBatchSize);
    if (unsyncedTasks.length > maxBatchSize) {
      logger.info(
        `Syncing ${tasksToSync.length} of ${unsyncedTasks.length} unsynced tasks (batch limit)`
      );
    }
    for (const task of tasksToSync) {
      try {
        const duplicateCheck = await duplicateDetector.checkForDuplicate(
          task.title,
          this.config.defaultTeamId
        );
        let linearIssue;
        if (duplicateCheck.isDuplicate && duplicateCheck.existingIssue) {
          logger.info(
            `Found existing Linear issue for "${task.title}": ${duplicateCheck.existingIssue.identifier} (${Math.round((duplicateCheck.similarity || 0) * 100)}% match)`
          );
          linearIssue = await duplicateDetector.mergeIntoExisting(
            duplicateCheck.existingIssue,
            task.title,
            this.formatDescriptionForLinear(task),
            `StackMemory Task ID: ${task.id}
Frame: ${task.frame_id}`
          );
        } else {
          linearIssue = await this.createLinearIssueFromTask(task);
        }
        const mapping = {
          stackmemoryId: task.id,
          linearId: linearIssue.id,
          linearIdentifier: linearIssue.identifier,
          lastSyncTimestamp: Date.now(),
          lastLinearUpdate: linearIssue.updatedAt,
          lastStackMemoryUpdate: task.timestamp * 1e3
        };
        this.mappings.set(task.id, mapping);
        this.updateTaskWithLinearRef(task.id, linearIssue);
        result.created++;
        logger.info(
          `Synced task to Linear: ${task.title} \u2192 ${linearIssue.identifier}`
        );
        await this.delay(rateLimitDelay);
      } catch (error) {
        const errorMsg = String(error);
        if (errorMsg.includes("rate limit") || errorMsg.includes("usage limit")) {
          logger.warn("Rate limit hit, stopping sync batch");
          result.errors.push("Rate limit reached - sync paused");
          break;
        }
        result.errors.push(`Failed to sync task ${task.id}: ${errorMsg}`);
        logger.error(
          `Failed to sync task ${task.id} to Linear:`,
          error
        );
      }
    }
    const modifiedTasks = this.getModifiedTasks();
    for (const task of modifiedTasks) {
      try {
        const mapping = this.mappings.get(task.id);
        if (!mapping) continue;
        await this.updateLinearIssueFromTask(task, mapping);
        mapping.lastSyncTimestamp = Date.now();
        mapping.lastStackMemoryUpdate = task.timestamp * 1e3;
        result.updated++;
        logger.info(`Updated Linear issue: ${mapping.linearIdentifier}`);
      } catch (error) {
        result.errors.push(
          `Failed to update Linear issue for task ${task.id}: ${String(error)}`
        );
        logger.error(
          `Failed to update Linear issue for task ${task.id}:`,
          error
        );
      }
    }
    return result;
  }
  /**
   * Sync tasks from Linear to StackMemory
   */
  async syncFromLinear() {
    const result = {
      created: 0,
      updated: 0,
      conflicts: [],
      errors: []
    };
    const importResult = await this.importFromLinear();
    result.created = importResult.imported;
    result.errors.push(...importResult.errors);
    for (const [taskId, mapping] of this.mappings) {
      try {
        const linearIssue = await this.linearClient.getIssue(mapping.linearId);
        if (!linearIssue) {
          result.errors.push(`Linear issue ${mapping.linearId} not found`);
          continue;
        }
        const linearUpdateTime = new Date(linearIssue.updatedAt).getTime();
        if (linearUpdateTime <= mapping.lastSyncTimestamp) {
          continue;
        }
        const task = this.taskStore.getTask(taskId);
        if (!task) {
          result.errors.push(`StackMemory task ${taskId} not found`);
          continue;
        }
        const stackMemoryUpdateTime = task.timestamp * 1e3;
        if (stackMemoryUpdateTime > mapping.lastSyncTimestamp && linearUpdateTime > mapping.lastSyncTimestamp) {
          result.conflicts.push({
            taskId,
            linearId: mapping.linearId,
            reason: "Both StackMemory and Linear were updated since last sync"
          });
          if (this.config.conflictResolution === "manual") {
            continue;
          }
        }
        const shouldUpdateFromLinear = this.shouldUpdateFromLinear(
          task,
          linearIssue,
          mapping,
          stackMemoryUpdateTime,
          linearUpdateTime
        );
        if (shouldUpdateFromLinear) {
          this.updateTaskFromLinearIssue(task, linearIssue);
          mapping.lastSyncTimestamp = Date.now();
          mapping.lastLinearUpdate = linearIssue.updatedAt;
          result.updated++;
          logger.info(`Updated StackMemory task from Linear: ${task.title}`);
        }
      } catch (error) {
        result.errors.push(
          `Failed to sync from Linear for task ${taskId}: ${String(error)}`
        );
        logger.error(
          `Failed to sync from Linear for task ${taskId}:`,
          error
        );
      }
    }
    return result;
  }
  /**
   * Create Linear issue from StackMemory task
   */
  async createLinearIssueFromTask(task) {
    const input = {
      title: task.title,
      description: this.formatDescriptionForLinear(task),
      teamId: this.config.defaultTeamId,
      priority: this.mapPriorityToLinear(task.priority),
      estimate: task.estimated_effort ? Math.ceil(task.estimated_effort / 60) : void 0,
      // Convert minutes to hours
      labelIds: this.mapTagsToLinear(task.tags)
    };
    return await this.linearClient.createIssue(input);
  }
  /**
   * Update Linear issue from StackMemory task
   */
  async updateLinearIssueFromTask(task, mapping) {
    const updates = {
      title: task.title,
      description: this.formatDescriptionForLinear(task),
      priority: this.mapPriorityToLinear(task.priority),
      estimate: task.estimated_effort ? Math.ceil(task.estimated_effort / 60) : void 0,
      stateId: await this.mapStatusToLinearState(task.status)
    };
    await this.linearClient.updateIssue(mapping.linearId, updates);
  }
  /**
   * Update StackMemory task from Linear issue
   */
  updateTaskFromLinearIssue(task, linearIssue) {
    const newStatus = this.mapLinearStateToStatus(linearIssue.state.type);
    if (newStatus !== task.status) {
      this.taskStore.updateTaskStatus(
        task.id,
        newStatus,
        "Updated from Linear"
      );
    }
  }
  /**
   * Check if task should be updated from Linear based on conflict resolution strategy
   */
  shouldUpdateFromLinear(task, linearIssue, mapping, stackMemoryUpdateTime, linearUpdateTime) {
    switch (this.config.conflictResolution) {
      case "linear_wins":
        return true;
      case "stackmemory_wins":
        return false;
      case "newest_wins":
        return linearUpdateTime > stackMemoryUpdateTime;
      case "manual":
        return false;
      default:
        return false;
    }
  }
  /**
   * Get tasks that haven't been synced to Linear yet
   */
  getUnsyncedTasks() {
    const activeTasks = this.taskStore.getActiveTasks();
    return activeTasks.filter(
      (task) => !this.mappings.has(task.id) && !task.external_refs?.linear
    );
  }
  /**
   * Get tasks that have been modified since last sync
   */
  getModifiedTasks() {
    const tasks = [];
    for (const [taskId, mapping] of this.mappings) {
      const task = this.taskStore.getTask(taskId);
      if (task && task.timestamp * 1e3 > mapping.lastSyncTimestamp) {
        tasks.push(task);
      }
    }
    return tasks;
  }
  /**
   * Update task with Linear reference
   */
  updateTaskWithLinearRef(taskId, linearIssue) {
    const task = this.taskStore.getTask(taskId);
    if (!task) return;
    logger.info(`Task ${taskId} mapped to Linear ${linearIssue.identifier}`);
  }
  // Mapping utilities
  formatDescriptionForLinear(task) {
    let description = task.description || "";
    description += `

---
**StackMemory Context:**
`;
    description += `- Task ID: ${task.id}
`;
    description += `- Frame: ${task.frame_id}
`;
    description += `- Created: ${new Date(task.created_at * 1e3).toISOString()}
`;
    if (task.tags.length > 0) {
      description += `- Tags: ${task.tags.join(", ")}
`;
    }
    if (task.depends_on.length > 0) {
      description += `- Dependencies: ${task.depends_on.join(", ")}
`;
    }
    return description;
  }
  mapPriorityToLinear(priority) {
    const map = {
      low: 1,
      // Low priority in Linear
      medium: 2,
      // Medium priority in Linear
      high: 3,
      // High priority in Linear
      urgent: 4
      // Urgent priority in Linear
    };
    return map[priority] || 2;
  }
  mapTagsToLinear(_tags) {
    return void 0;
  }
  mapLinearStateToStatus(linearStateType) {
    switch (linearStateType) {
      case "backlog":
      case "unstarted":
        return "pending";
      case "started":
        return "in_progress";
      case "completed":
        return "completed";
      case "cancelled":
        return "cancelled";
      default:
        return "pending";
    }
  }
  async mapStatusToLinearState(status) {
    try {
      const team = await this.linearClient.getTeam();
      const states = await this.linearClient.getWorkflowStates(team.id);
      const targetStateType = this.getLinearStateTypeFromStatus(status);
      const matchingState = states.find(
        (state) => state.type === targetStateType
      );
      return matchingState?.id;
    } catch (error) {
      logger.warn(
        "Failed to map status to Linear state:",
        error instanceof Error ? { error } : void 0
      );
      return void 0;
    }
  }
  getLinearStateTypeFromStatus(status) {
    switch (status) {
      case "pending":
        return "unstarted";
      case "in_progress":
        return "started";
      case "completed":
        return "completed";
      case "cancelled":
        return "cancelled";
      case "blocked":
        return "unstarted";
      // Map blocked to unstarted in Linear
      default:
        return "unstarted";
    }
  }
  // Persistence for mappings
  loadMappings() {
    this.mappings.clear();
    if (existsSync(this.mappingsPath)) {
      try {
        const data = readFileSync(this.mappingsPath, "utf8");
        const mappingsArray = JSON.parse(data);
        for (const mapping of mappingsArray) {
          this.mappings.set(mapping.stackmemoryId, mapping);
        }
        logger.info(`Loaded ${this.mappings.size} task mappings from disk`);
      } catch (error) {
        logger.warn("Failed to load mappings, starting fresh");
      }
    }
  }
  saveMappings() {
    try {
      const mappingsArray = Array.from(this.mappings.values());
      writeFileSync(this.mappingsPath, JSON.stringify(mappingsArray, null, 2));
      logger.info(`Saved ${this.mappings.size} task mappings to disk`);
    } catch (error) {
      logger.error("Failed to save mappings:", error);
    }
  }
  /**
   * Import all issues from Linear to local task store
   */
  async importFromLinear() {
    const result = { imported: 0, skipped: 0, errors: [] };
    try {
      if (!this.config.defaultTeamId) {
        const team = await this.linearClient.getTeam();
        this.config.defaultTeamId = team.id;
        logger.info(`Using Linear team: ${team.name} (${team.key})`);
      }
      const issues = await this.linearClient.getIssues({
        teamId: this.config.defaultTeamId,
        limit: 100
      });
      logger.info(`Found ${issues.length} issues in Linear`);
      const linearIdToTaskId = /* @__PURE__ */ new Map();
      for (const [taskId, mapping] of this.mappings) {
        linearIdToTaskId.set(mapping.linearId, taskId);
      }
      for (const issue of issues) {
        try {
          if (linearIdToTaskId.has(issue.id)) {
            result.skipped++;
            continue;
          }
          const taskId = await this.createTaskFromLinearIssue(issue);
          if (taskId) {
            const mapping = {
              stackmemoryId: taskId,
              linearId: issue.id,
              linearIdentifier: issue.identifier,
              lastSyncTimestamp: Date.now(),
              lastLinearUpdate: issue.updatedAt,
              lastStackMemoryUpdate: Date.now()
            };
            this.mappings.set(taskId, mapping);
            result.imported++;
            logger.info(`Imported ${issue.identifier}: ${issue.title}`);
          }
        } catch (error) {
          result.errors.push(
            `Failed to import ${issue.identifier}: ${String(error)}`
          );
          logger.error(`Failed to import ${issue.identifier}:`, error);
        }
      }
      this.saveMappings();
    } catch (error) {
      result.errors.push(`Import failed: ${String(error)}`);
      logger.error("Linear import failed:", error);
    }
    return result;
  }
  /**
   * Create a local task from a Linear issue
   */
  async createTaskFromLinearIssue(issue) {
    try {
      const priority = this.mapLinearPriorityToLocal(issue.priority);
      let description = issue.description || "";
      description += `

---
**Linear:** ${issue.identifier} | ${issue.url}`;
      const labels = Array.isArray(issue.labels) ? issue.labels : issue.labels?.nodes || [];
      const tags = labels.map((l) => l.name);
      if (tags.length === 0) tags.push("linear");
      const taskId = this.taskStore.createTask({
        title: `[${issue.identifier}] ${issue.title}`,
        description,
        priority,
        frameId: "linear-import",
        tags,
        estimatedEffort: issue.estimate ? issue.estimate * 60 : void 0
      });
      const status = this.mapLinearStateToStatus(issue.state.type);
      if (status !== "pending") {
        this.taskStore.updateTaskStatus(
          taskId,
          status,
          `Imported from Linear as ${status}`
        );
      }
      return taskId;
    } catch (error) {
      logger.error(
        `Failed to create task from Linear issue ${issue.identifier}: ${String(error)}`
      );
      return null;
    }
  }
  /**
   * Map Linear priority (0-4) to local TaskPriority
   */
  mapLinearPriorityToLocal(priority) {
    switch (priority) {
      case 1:
        return "urgent";
      case 2:
        return "high";
      case 3:
        return "medium";
      case 4:
        return "low";
      default:
        return "medium";
    }
  }
}
const DEFAULT_SYNC_CONFIG = {
  enabled: false,
  direction: "bidirectional",
  autoSync: true,
  conflictResolution: "newest_wins",
  syncInterval: 15,
  // minutes
  maxBatchSize: 10,
  // max tasks per sync batch
  rateLimitDelay: 500
  // 500ms between API calls
};
class LinearDuplicateDetector {
  linearClient;
  titleCache = /* @__PURE__ */ new Map();
  cacheExpiry = 5 * 60 * 1e3;
  // 5 minutes
  lastCacheRefresh = 0;
  constructor(linearClient) {
    this.linearClient = linearClient;
  }
  /**
   * Search for existing Linear issues with similar titles
   */
  async searchByTitle(title, teamId) {
    const normalizedTitle = this.normalizeTitle(title);
    if (this.isCacheValid()) {
      const cached = this.titleCache.get(normalizedTitle);
      if (cached) return cached;
    }
    try {
      const allIssues = await this.linearClient.getIssues({
        teamId,
        limit: 100
        // Use smaller limit to avoid API errors
      });
      const matchingIssues = allIssues.filter((issue) => {
        const issueNormalized = this.normalizeTitle(issue.title);
        if (issueNormalized === normalizedTitle) return true;
        const similarity = this.calculateSimilarity(normalizedTitle, issueNormalized);
        return similarity > 0.85;
      });
      this.titleCache.set(normalizedTitle, matchingIssues);
      this.lastCacheRefresh = Date.now();
      return matchingIssues;
    } catch (error) {
      logger.error("Failed to search Linear issues by title:", error);
      return [];
    }
  }
  /**
   * Check if a task title would create a duplicate in Linear
   */
  async checkForDuplicate(title, teamId) {
    const existingIssues = await this.searchByTitle(title, teamId);
    if (existingIssues.length === 0) {
      return { isDuplicate: false };
    }
    let bestMatch;
    let bestSimilarity = 0;
    for (const issue of existingIssues) {
      const similarity = this.calculateSimilarity(
        this.normalizeTitle(title),
        this.normalizeTitle(issue.title)
      );
      if (similarity > bestSimilarity) {
        bestSimilarity = similarity;
        bestMatch = issue;
      }
    }
    return {
      isDuplicate: true,
      existingIssue: bestMatch,
      similarity: bestSimilarity
    };
  }
  /**
   * Merge task content into existing Linear issue
   */
  async mergeIntoExisting(existingIssue, newTitle, newDescription, additionalContext) {
    try {
      let mergedDescription = existingIssue.description || "";
      if (newDescription && !mergedDescription.includes(newDescription)) {
        mergedDescription += `

## Additional Context (${(/* @__PURE__ */ new Date()).toISOString()})
`;
        mergedDescription += newDescription;
      }
      if (additionalContext) {
        mergedDescription += `

---
${additionalContext}`;
      }
      const updateQuery = `
        mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
          issueUpdate(id: $id, input: $input) {
            issue {
              id
              identifier
              title
              description
              updatedAt
            }
          }
        }
      `;
      const variables = {
        id: existingIssue.id,
        input: {
          description: mergedDescription
        }
      };
      const response = await this.linearClient.graphql(updateQuery, variables);
      const updatedIssue = response.issueUpdate?.issue;
      if (updatedIssue) {
        logger.info(
          `Merged content into existing Linear issue ${existingIssue.identifier}: ${existingIssue.title}`
        );
        return updatedIssue;
      }
      return existingIssue;
    } catch (error) {
      logger.error("Failed to merge into existing Linear issue:", error);
      return existingIssue;
    }
  }
  /**
   * Normalize title for comparison
   */
  normalizeTitle(title) {
    return title.toLowerCase().trim().replace(/\s+/g, " ").replace(/[^\w\s-]/g, "").replace(/^(sta|eng|bug|feat|task|tsk)[-\s]\d+[-\s:]*/, "").trim();
  }
  /**
   * Calculate similarity between two strings (Levenshtein distance based)
   */
  calculateSimilarity(str1, str2) {
    if (str1 === str2) return 1;
    if (str1.length === 0 || str2.length === 0) return 0;
    const distance = this.levenshteinDistance(str1, str2);
    const maxLength = Math.max(str1.length, str2.length);
    return 1 - distance / maxLength;
  }
  /**
   * Calculate Levenshtein distance between two strings
   */
  levenshteinDistance(str1, str2) {
    const m = str1.length;
    const n = str2.length;
    const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
    for (let i = 0; i <= m; i++) dp[i][0] = i;
    for (let j = 0; j <= n; j++) dp[0][j] = j;
    for (let i = 1; i <= m; i++) {
      for (let j = 1; j <= n; j++) {
        if (str1[i - 1] === str2[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1];
        } else {
          dp[i][j] = 1 + Math.min(
            dp[i - 1][j],
            // deletion
            dp[i][j - 1],
            // insertion
            dp[i - 1][j - 1]
            // substitution
          );
        }
      }
    }
    return dp[m][n];
  }
  /**
   * Check if cache is still valid
   */
  isCacheValid() {
    return Date.now() - this.lastCacheRefresh < this.cacheExpiry;
  }
  /**
   * Clear the title cache
   */
  clearCache() {
    this.titleCache.clear();
    this.lastCacheRefresh = 0;
  }
}
export {
  DEFAULT_SYNC_CONFIG,
  LinearDuplicateDetector,
  LinearSyncEngine
};
//# sourceMappingURL=sync.js.map
