import { readdirSync, existsSync } from "fs";
import { join } from "path";
import { AsanaClient } from "../asana/client.js";
import { PiExecutor } from "../pi/executor.js";
import { logger } from "../logger.js";
import { FileLock } from "./lock.js";
import type { Config } from "../config.js";
import type { AsanaTask, AsanaSection } from "../asana/types.js";

/**
 * Section classification for the Bug Intake board.
 *
 * PICKUP  = sections we actively pull tasks FROM to execute
 * EXECUTE = "Under Review" / "In Progress" — where we move tasks during execution
 * DONE    = where completed tasks land
 * SKIP    = sections we never touch (deferred, won't fix)
 */
const SECTIONS = {
  PICKUP: ["New Bugs", "Ready for Development"],
  EXECUTE: ["Under Review", "In Progress"],
  DONE: ["Resolved", "Done", "Complete", "Completed"],
  SKIP: ["Deferred", "Won't Fix", "Awaiting More Info"],
};

/**
 * Map of keywords in task names/notes → project directory names.
 * Used to route tasks to the correct codebase for execution.
 */
const PROJECT_KEYWORDS: Record<string, string[]> = {
  "legacy-donation-system-laravel": [
    "laravel", "legacy", "donation system", "receipt", "email receipt",
    "pledge", "donor", "donation", "fundrais", "campaign", "appeal",
    "checkout", "payment", "stripe", "paypal", "gocardless",
    "cross sell", "team page", "ben nevis",
  ],
  "charity-right-uk-v2": [
    "charity-right", "charityright", "website", "frontend", "next.js",
    "nextjs", "landing", "home page", "UI", "layout", "styling",
  ],
  "checkout-v2": [
    "checkout-v2", "checkout v2", "new checkout", "payment flow",
  ],
  "donation-dashboard": [
    "dashboard", "admin", "analytics", "reporting", "reports",
    "export", "data export",
  ],
  "command-center": [
    "command center", "command-center", "ops", "monitoring",
  ],
  "charityright-sync": [
    "sync", "n3o", "engage", "import", "county", "state", "mapping",
  ],
  "enthuse-db-sync-v2": [
    "enthuse", "enthuse sync", "db sync",
  ],
  "launchgood-sync": [
    "launchgood", "launch good",
  ],
  "marketing-site": [
    "marketing", "blog", "wordpress", "seo", "content",
  ],
};

export class TaskLoop {
  private asana: AsanaClient;
  private pi: PiExecutor;
  private config: Config;
  private lock: FileLock;
  private running = false;
  private timer: ReturnType<typeof setInterval> | null = null;
  private userTaskListGid: string | null = null;
  private workspaceGid: string = "";

  // Cache: track tasks we already verified as "not done" to avoid re-checking
  private verifiedNotDone: Map<string, number> = new Map(); // gid → timestamp
  private readonly VERIFY_CACHE_TTL = 30 * 60 * 1000; // 30 min

  // Cache: sections per project
  private sectionCache: Map<string, { sections: AsanaSection[]; ts: number }> = new Map();
  private readonly SECTION_CACHE_TTL = 5 * 60 * 1000; // 5 min

  // Track consecutive failures to back off
  private consecutiveFailures = 0;
  private readonly MAX_BACKOFF_MULTIPLIER = 5;

  constructor(config: Config) {
    this.config = config;
    this.asana = new AsanaClient(config.asanaAccessToken);
    this.pi = new PiExecutor(config);
    this.lock = new FileLock("task-loop");
  }

  async start() {
    this.running = true;
    logger.info("task-loop", "Starting Asana task polling loop", {
      interval: `${this.config.pollIntervalSec}s`,
      project: this.config.asanaProjectGid,
      projectsPath: this.getProjectsDir(),
    });

    if (!this.config.asanaProjectGid) {
      await this.discoverProject();
    }

    // Log available projects for debugging
    const projects = this.getAvailableProjects();
    logger.info("task-loop", `Available codebases: ${projects.join(", ") || "NONE"}`);

    await this.poll();
    this.timer = setInterval(() => this.poll(), this.config.pollIntervalSec * 1000);
  }

  stop() {
    this.running = false;
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
    this.pi.abort();
    this.lock.release();
  }

  // ─── Project resolution ──────────────────────────────────────────────

  private getProjectsDir(): string {
    return join(this.config.targetProjectPath, this.config.projectsPath);
  }

  private getAvailableProjects(): string[] {
    const dir = this.getProjectsDir();
    if (!existsSync(dir)) return [];
    return readdirSync(dir, { withFileTypes: true })
      .filter((d) => d.isDirectory() && !d.name.startsWith("."))
      .map((d) => d.name);
  }

  /**
   * Determine which project codebase a task should execute against.
   * Returns the absolute path to the project directory, or the
   * targetProjectPath as fallback.
   */
  private resolveProjectForTask(task: AsanaTask): { projectDir: string; projectName: string } {
    const searchText = `${task.name} ${task.notes || ""}`.toLowerCase();
    const available = this.getAvailableProjects();

    // First: check if the task name contains a [project] tag (from improvement loop)
    const tagMatch = task.name.match(/\[([^\]]+)\]/);
    if (tagMatch) {
      const tagName = tagMatch[1].toLowerCase().replace(/\s+/g, "-");
      const exact = available.find((p) => p.toLowerCase() === tagName);
      if (exact) {
        return {
          projectDir: join(this.getProjectsDir(), exact),
          projectName: exact,
        };
      }
    }

    // Second: keyword matching
    let bestMatch: { project: string; score: number } | null = null;

    for (const [project, keywords] of Object.entries(PROJECT_KEYWORDS)) {
      if (!available.includes(project)) continue;
      let score = 0;
      for (const keyword of keywords) {
        if (searchText.includes(keyword.toLowerCase())) {
          score += keyword.length; // Longer matches score higher
        }
      }
      if (score > 0 && (!bestMatch || score > bestMatch.score)) {
        bestMatch = { project, score };
      }
    }

    if (bestMatch) {
      return {
        projectDir: join(this.getProjectsDir(), bestMatch.project),
        projectName: bestMatch.project,
      };
    }

    // Fallback: use the main project path
    return {
      projectDir: this.config.targetProjectPath,
      projectName: "unknown",
    };
  }

  // ─── Discovery ───────────────────────────────────────────────────────

  private async discoverProject() {
    try {
      logger.info("task-loop", "No project GID configured, discovering...");
      const workspaces = await this.asana.getWorkspaces();

      if (workspaces.length === 0) {
        logger.error("task-loop", "No Asana workspaces found");
        return;
      }

      for (const ws of workspaces) {
        const projects = await this.asana.getProjects(ws.gid);
        logger.info("task-loop", `Workspace "${ws.name}" has ${projects.length} project(s)`);

        if (projects.length > 0 && !this.config.asanaProjectGid) {
          this.config.asanaProjectGid = projects[0].gid;
          this.workspaceGid = ws.gid;
          logger.info("task-loop", `Auto-selected project: ${projects[0].name} (${projects[0].gid})`);
        }
      }

      // Cache user task list
      try {
        const me = await this.asana.getMe();
        if (this.workspaceGid) {
          const utl = await this.asana.getUserTaskList(me.gid, this.workspaceGid);
          this.userTaskListGid = utl.gid;
          logger.info("task-loop", `Cached user task list: ${utl.name} (${utl.gid})`);
        }
      } catch (error: any) {
        logger.warn("task-loop", `Could not get user task list: ${error.message}`);
      }
    } catch (error: any) {
      logger.error("task-loop", "Failed to discover Asana project", { error: error.message });
    }
  }

  // ─── Main poll loop ──────────────────────────────────────────────────

  private async poll() {
    if (!this.running || !this.config.asanaProjectGid) return;
    if (!this.lock.acquire()) {
      logger.debug("task-loop", "Skipping poll — another execution in progress");
      return;
    }

    try {
      // Step 1: Quick cleanup — move completed My Tasks to Done sections
      await this.cleanupCompletedMyTasks();

      // Step 2: Pick up tasks to execute
      // Check PICKUP sections first (New Bugs, Ready for Development)
      // Then check EXECUTE sections (Under Review) for tasks that haven't been attempted
      logger.info("task-loop", "Polling for Asana tasks...");

      let tasks = await this.getTasksFromSections(this.config.asanaProjectGid, SECTIONS.PICKUP);

      if (tasks.length === 0) {
        // Also check Under Review — these are real bugs waiting for someone to fix them
        tasks = await this.getTasksFromSections(this.config.asanaProjectGid, SECTIONS.EXECUTE);
        
        // Filter out tasks we recently attempted (check comments for Pi Worker activity)
        if (tasks.length > 0) {
          tasks = await this.filterUnatttemptedTasks(tasks);
        }
      }

      if (tasks.length === 0) {
        logger.info("task-loop", "No pending tasks found");
        this.lock.release();
        return;
      }

      // Pick the first task
      const task = tasks[0];
      const { projectDir, projectName } = this.resolveProjectForTask(task);
      logger.info("task-loop", `Picked task: "${task.name}"`, {
        gid: task.gid,
        project: projectName,
        projectDir,
      });

      await this.executeTask(task, projectDir, projectName);
    } catch (error: any) {
      logger.error("task-loop", "Poll failed", { error: error.message });
      this.consecutiveFailures++;
    } finally {
      this.lock.release();
    }
  }

  /**
   * Filter out tasks that Pi Worker has already commented on (attempted).
   * Prevents re-attempting failed tasks every cycle.
   */
  private async filterUnatttemptedTasks(tasks: AsanaTask[]): Promise<AsanaTask[]> {
    const unattempted: AsanaTask[] = [];

    for (const task of tasks) {
      try {
        const comments = await this.asana.getComments(task.gid);
        const hasWorkerComment = comments.some(
          (c) => c.text && c.text.includes("Pi Worker")
        );
        if (!hasWorkerComment) {
          unattempted.push(task);
        } else {
          logger.debug("task-loop", `Skipping already-attempted task: "${task.name}"`);
        }
      } catch {
        // If we can't read comments, include the task
        unattempted.push(task);
      }
    }

    return unattempted;
  }

  // ─── Cleanup ─────────────────────────────────────────────────────────

  private async cleanupCompletedMyTasks() {
    if (!this.userTaskListGid) return;

    try {
      const myTasks = await this.asana.getUserTasks(this.userTaskListGid);
      const completedTasks = myTasks.filter((t) => t.completed);

      if (completedTasks.length === 0) return;

      logger.info("task-loop", `Cleaning ${completedTasks.length} completed tasks from My Tasks`);

      for (const task of completedTasks) {
        try {
          if (task.memberships && task.memberships.length > 0) {
            for (const membership of task.memberships) {
              const projectGid = membership.project?.gid;
              const currentSection = membership.section?.name?.toLowerCase() || "";

              if (currentSection.includes("done") || currentSection.includes("resolved") || currentSection.includes("complete") || currentSection.includes("won't")) {
                continue;
              }

              if (projectGid) {
                const doneSection = await this.findDoneSection(projectGid);
                if (doneSection) {
                  await this.asana.moveTaskToSection(task.gid, doneSection.gid);
                  logger.info("task-loop", `Moved to ${doneSection.name}: ${task.name}`);
                }
              }
            }
          }

          await this.asana.updateTask(task.gid, { assignee_status: "later" });
        } catch (e: any) {
          logger.debug("task-loop", `Could not clean task ${task.name}: ${e.message}`);
        }
      }
    } catch (error: any) {
      logger.warn("task-loop", `My Tasks cleanup error: ${error.message}`);
    }
  }

  // ─── Task execution ─────────────────────────────────────────────────

  private async executeTask(task: AsanaTask, projectDir: string, projectName: string) {
    try {
      // Step 1: Move to Under Review
      const executeSection = await this.findFirstSection(this.config.asanaProjectGid, SECTIONS.EXECUTE);
      if (executeSection) await this.asana.moveTaskToSection(task.gid, executeSection.gid);

      await this.asana.addComment(
        task.gid,
        `🤖 Pi Worker picked up this task.\nProject: \`${projectName}\`\nStarted: ${new Date().toISOString()}`
      );

      // Step 2: Build context with project awareness
      const context = this.buildTaskContext(task, projectDir, projectName);

      // Step 3: Pull latest code
      await this.gitPull(projectDir, projectName);

      // Step 4: Execute with Pi in the correct project directory
      const result = await this.pi.execute(
        task.name + (task.notes ? `\n\nDetails:\n${task.notes}` : ""),
        context,
        undefined,
        projectDir, // Execute in the project directory
      );

      // Step 5: Post-verify
      let verified = false;
      if (result.success && result.toolCalls > 0) {
        const postCheck = await this.verifyTaskDone(task, projectDir);
        verified = postCheck.done;
      }

      const statusEmoji = result.success ? (verified ? "✅" : "⚠️") : "❌";
      const verificationNote = result.success
        ? (verified
            ? "Post-execution verification: **PASSED**"
            : "Post-execution verification: COULD NOT CONFIRM — please review manually")
        : "";

      const comment = [
        `${statusEmoji} Pi Worker ${result.success ? "completed" : "failed"} this task.`,
        `Project: \`${projectName}\``,
        verificationNote,
        `Duration: ${(result.durationMs / 1000).toFixed(1)}s`,
        `Tool calls: ${result.toolCalls}`,
        result.error ? `Error: ${result.error}` : "",
        "",
        "Output (truncated):",
        "```",
        result.output.substring(0, 3000),
        "```",
      ].filter(Boolean).join("\n");

      await this.asana.addComment(task.gid, comment);

      if (result.success) {
        const doneSection = await this.findFirstSection(this.config.asanaProjectGid, SECTIONS.DONE);
        if (doneSection) await this.asana.moveTaskToSection(task.gid, doneSection.gid);
        await this.asana.updateTask(task.gid, { completed: true });
        logger.info("task-loop", `Task completed: ${task.name}`, { verified, project: projectName });
        this.consecutiveFailures = 0;
      } else {
        // Leave in Under Review — don't move back to New Bugs
        // The filterUnattemptedTasks check will skip it on future polls
        logger.error("task-loop", `Task failed: ${task.name}`, { error: result.error, project: projectName });
        this.consecutiveFailures++;
      }
    } catch (error: any) {
      logger.error("task-loop", `Task execution error: ${task.name}`, { error: error.message });
      this.consecutiveFailures++;
      try {
        await this.asana.addComment(task.gid, `❌ Pi Worker crashed: ${error.message}`);
      } catch {}
    }
  }

  private async verifyTaskDone(task: AsanaTask, projectDir: string): Promise<{ done: boolean; evidence: string }> {
    try {
      const verifyPrompt = `CHECK if this task is already done. Be FAST — spend max 30 seconds.
Do NOT fix anything. Do NOT make changes. Just check and report.

Task: ${task.name}
${task.notes ? `Details: ${task.notes}` : ""}

Quick checks only:
- ls and grep relevant files/dirs
- Check if a fix/feature is already in the code
- Check git log for recent relevant commits
- Check running services if relevant (curl localhost)
- Do NOT run find on large directories
- Do NOT clone repos or install anything

RESPOND EXACTLY:
DONE: true or false
EVIDENCE: one line why`;

      const result = await this.pi.execute(verifyPrompt, undefined, 120_000, projectDir);

      if (!result.success) {
        return { done: false, evidence: "Verification check failed to run" };
      }

      const output = result.output.trim();
      const doneMatch = output.match(/DONE:\s*(true|false)/i);
      const evidenceMatch = output.match(/EVIDENCE:\s*(.+)/is);

      const isDone = doneMatch ? doneMatch[1].toLowerCase() === "true" : false;
      const evidence = evidenceMatch ? evidenceMatch[1].trim().substring(0, 1000) : output.substring(0, 1000);

      return { done: isDone, evidence };
    } catch (error: any) {
      logger.error("task-loop", `Verification error: ${error.message}`);
      return { done: false, evidence: `Verification error: ${error.message}` };
    }
  }

  private async gitPull(projectDir: string, projectName: string) {
    try {
      const proc = Bun.spawn({
        cmd: ["git", "-C", projectDir, "pull", "--ff-only"],
        stdout: "pipe",
        stderr: "pipe",
      });
      await proc.exited;
      logger.debug("task-loop", `Git pull: ${projectName}`);
    } catch {
      logger.debug("task-loop", `Git pull failed for ${projectName}`);
    }
  }

  // ─── Section helpers ─────────────────────────────────────────────────

  private async findSectionCached(projectGid: string, sectionName: string): Promise<AsanaSection | null> {
    const cached = this.sectionCache.get(projectGid);
    let sections: AsanaSection[];

    if (cached && Date.now() - cached.ts < this.SECTION_CACHE_TTL) {
      sections = cached.sections;
    } else {
      sections = await this.asana.getSections(projectGid);
      this.sectionCache.set(projectGid, { sections, ts: Date.now() });
    }

    return sections.find((s) => s.name.toLowerCase().includes(sectionName.toLowerCase())) || null;
  }

  private async findDoneSection(projectGid: string): Promise<AsanaSection | null> {
    for (const name of SECTIONS.DONE) {
      const section = await this.findSectionCached(projectGid, name);
      if (section) return section;
    }
    return null;
  }

  private async findFirstSection(projectGid: string, names: string[]): Promise<AsanaSection | null> {
    for (const name of names) {
      const section = await this.findSectionCached(projectGid, name);
      if (section) return section;
    }
    return null;
  }

  private async getTasksFromSections(projectGid: string, sectionNames: string[]): Promise<AsanaTask[]> {
    const allTasks: AsanaTask[] = [];
    for (const name of sectionNames) {
      const section = await this.findSectionCached(projectGid, name);
      if (!section) continue;
      const tasks = await this.asana.getSectionTasks(section.gid);
      const incomplete = tasks.filter((t) => !t.completed);
      allTasks.push(...incomplete);
    }
    return allTasks;
  }

  // ─── Context building ───────────────────────────────────────────────

  private buildTaskContext(task: AsanaTask, projectDir: string, projectName: string): string {
    const parts = [
      `Asana Task: ${task.name}`,
      task.notes ? `Description: ${task.notes}` : "",
      task.due_on ? `Due: ${task.due_on}` : "",
      task.tags?.length > 0 ? `Tags: ${task.tags.map((t) => t.name).join(", ")}` : "",
      "",
      `You are working on the "${projectName}" codebase.`,
      `The code is at: ${projectDir}`,
      "",
      `IMPORTANT RULES:`,
      `- Read relevant files before making any changes`,
      `- Make small, focused changes that fix the specific bug`,
      `- Test your changes (run type checks, linters, or curl endpoints)`,
      `- Commit with a clear message referencing the Asana task`,
      `- Do NOT modify .env files or credentials`,
      `- Do NOT run destructive commands (rm -rf, DROP DATABASE)`,
      `- If the bug is in the database data (not code), describe what SQL fix is needed but do NOT run it`,
    ];
    return parts.filter(Boolean).join("\n");
  }
}
