/**
 * LocalProjectModule - Sole owner of project state and persistence for ALL projects.
 *
 * Manages internal state (projects map), persists project configs to disk,
 * and responds to resolve hook points across operations.
 *
 * Hook registrations:
 * - resolve-project: shared project resolution (projectPath → projectId + projectName)
 * - project:open  → resolve:  validate .git exists for local paths
 * - project:open  → register: generate ID, persist, add to internal state (all projects)
 * - project:close → resolve:  look up projectPath in config to get remoteUrl
 * - project:close → close:    remove from internal state and config (all projects)
 * - app:start     → start:    load ALL saved project configs
 */

import * as crypto from "node:crypto";
import nodePath from "path";
import type { IntentModule } from "../intents/lib/module";
import type { HookContext } from "../intents/lib/operation";
import type { ProjectId } from "../shared/api/types";
import { Path } from "../utils/path/path";
import { projectDirName } from "../boundaries/platform/paths";
import type { FileSystemBoundary } from "../boundaries/platform/filesystem";
import type { ProjectConfig } from "../shared/types/project";
import { CURRENT_PROJECT_VERSION } from "../shared/types/project";
import { ProjectStoreError, getErrorMessage } from "../shared/errors/service-errors";
import type { GitWorktreeProvider } from "../boundaries/platform/git-worktree-provider";
import {
  OPEN_PROJECT_OPERATION_ID,
  type OpenProjectIntent,
  type PrepareHookResult,
  type ResolveHookResult,
  type RegisterHookInput,
  type RegisterHookResult,
} from "../intents/open-project";
import type { DialogManager } from "./dialog-manager";
import type { IGitClient } from "../boundaries/platform/git-client";
import {
  CLOSE_PROJECT_OPERATION_ID,
  type CloseProjectIntent,
  type CloseResolveHookResult,
  type CloseHookInput,
  type CloseHookResult,
} from "../intents/close-project";
import { APP_READY_OPERATION_ID, type LoadProjectsResult } from "../intents/app-ready";
import {
  RESOLVE_PROJECT_OPERATION_ID,
  type ResolveHookInput as ResolveProjectHookInput,
  type ResolveHookResult as ResolveProjectHookResult,
} from "../intents/resolve-project";
import {
  LIST_PROJECTS_OPERATION_ID,
  type ListProjectsHookResult,
  type ListProjectsHookEntry,
} from "../intents/list-projects";

// =============================================================================
// Types
// =============================================================================

/**
 * Internal representation of a project tracked by this module.
 * No remoteUrl — LocalProjectModule is unaware of remote concerns.
 */
export interface LocalProject {
  readonly id: ProjectId;
  readonly name: string;
  readonly path: Path;
}

/**
 * Dependencies for LocalProjectModule.
 */
export interface LocalProjectModuleDeps {
  readonly projectsDir: string;
  readonly fs: Pick<
    FileSystemBoundary,
    "readdir" | "readFile" | "writeFile" | "mkdir" | "unlink" | "rm" | "rename"
  >;
  readonly gitWorktreeProvider: Pick<GitWorktreeProvider, "validateRepository">;
  readonly dialogManager: DialogManager;
  readonly gitClient: Pick<IGitClient, "isRepositoryRoot" | "init">;
}

// =============================================================================
// Private ID Generation
// =============================================================================

function normalizePathForId(absolutePath: string): string {
  let normalized = nodePath.normalize(absolutePath);
  normalized = normalized.replace(/\\/g, "/");
  normalized = normalized.replace(/\/+/g, "/");
  if (normalized.length > 1 && normalized.endsWith("/")) {
    normalized = normalized.slice(0, -1);
  }
  if (process.platform === "win32") {
    normalized = normalized.toLowerCase();
  }
  return normalized;
}

function generateProjectId(absolutePath: string): ProjectId {
  const normalizedPath = normalizePathForId(absolutePath);
  const basename = normalizedPath.split("/").pop() ?? "";
  const safeName =
    basename
      .replace(/[^a-zA-Z0-9]/g, "-")
      .replace(/-+/g, "-")
      .replace(/^-|-$/g, "") || "root";
  const hash = crypto.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
  return `${safeName}-${hash}` as ProjectId;
}

// =============================================================================
// Private Persistence Helpers
// =============================================================================

type ProjectFs = LocalProjectModuleDeps["fs"];

async function saveProject(
  fs: ProjectFs,
  projectsDir: string,
  projectPath: string,
  remoteUrl?: string
): Promise<void> {
  const normalizedPath = new Path(projectPath).toString();
  const projectDir = nodePath.join(projectsDir, projectDirName(normalizedPath));
  const configPath = nodePath.join(projectDir, "config.json");

  const config: ProjectConfig = {
    version: CURRENT_PROJECT_VERSION,
    path: normalizedPath,
    ...(remoteUrl !== undefined && { remoteUrl }),
  };

  try {
    await fs.mkdir(projectDir);
    await fs.writeFile(configPath, JSON.stringify(config, null, 2));
  } catch (error: unknown) {
    throw new ProjectStoreError(`Failed to save project: ${getErrorMessage(error)}`);
  }
}

async function internalLoadAllProjectConfigs(
  fs: ProjectFs,
  projectsDir: string
): Promise<readonly { config: ProjectConfig; entryName: string }[]> {
  const results: { config: ProjectConfig; entryName: string }[] = [];

  try {
    const entries = await fs.readdir(projectsDir);

    for (const entry of entries) {
      if (!entry.isDirectory) {
        continue;
      }

      const configPath = nodePath.join(projectsDir, entry.name, "config.json");

      try {
        const content = await fs.readFile(configPath);
        const parsed: unknown = JSON.parse(content);

        if (
          typeof parsed === "object" &&
          parsed !== null &&
          "path" in parsed &&
          typeof (parsed as Record<string, unknown>).path === "string"
        ) {
          const rawPath = (parsed as { path: string }).path;
          try {
            const normalizedPath = new Path(rawPath).toString();
            const rawRemoteUrl = (parsed as { remoteUrl?: string }).remoteUrl;

            const config: ProjectConfig = {
              version: (parsed as { version?: number }).version ?? 1,
              path: normalizedPath,
              ...(rawRemoteUrl !== undefined && { remoteUrl: rawRemoteUrl }),
            };
            results.push({ config, entryName: entry.name });
          } catch {
            // Invalid path format - skip this entry
            continue;
          }
        }
      } catch {
        // Skip invalid entries (ENOENT, malformed JSON, etc.)
        continue;
      }
    }
  } catch {
    // Directory doesn't exist or other error - return empty array
    return [];
  }

  return results;
}

async function loadAllProjectConfigs(
  fs: ProjectFs,
  projectsDir: string
): Promise<readonly ProjectConfig[]> {
  const internal = await internalLoadAllProjectConfigs(fs, projectsDir);
  return internal.map((entry) => entry.config);
}

async function getProjectConfig(
  fs: ProjectFs,
  projectsDir: string,
  projectPath: string
): Promise<ProjectConfig | undefined> {
  const normalizedPath = new Path(projectPath).toString();

  // First, try the standard path-hashed location (most common case)
  const dirName = projectDirName(normalizedPath);
  const projectDir = nodePath.join(projectsDir, dirName);
  const configPath = nodePath.join(projectDir, "config.json");

  try {
    const content = await fs.readFile(configPath);
    const parsed: unknown = JSON.parse(content);

    if (
      typeof parsed === "object" &&
      parsed !== null &&
      "path" in parsed &&
      typeof (parsed as Record<string, unknown>).path === "string"
    ) {
      const rawPath = (parsed as { path: string }).path;
      const rawRemoteUrl = (parsed as { remoteUrl?: string }).remoteUrl;

      const config: ProjectConfig = {
        version: (parsed as { version?: number }).version ?? 1,
        path: new Path(rawPath).toString(),
        ...(rawRemoteUrl !== undefined && { remoteUrl: rawRemoteUrl }),
      };
      return config;
    }
  } catch {
    // Config not found in standard location - try scanning all configs
  }

  // Fallback: scan all configs to find one with matching path
  // This handles cloned projects where config is in URL-hashed directory
  const allConfigs = await loadAllProjectConfigs(fs, projectsDir);
  for (const config of allConfigs) {
    if (config.path === normalizedPath) {
      return config;
    }
  }

  return undefined;
}

async function removeProject(
  fs: ProjectFs,
  projectsDir: string,
  projectPath: string
): Promise<void> {
  const dirName = projectDirName(projectPath);
  const projectDir = nodePath.join(projectsDir, dirName);
  const configPath = nodePath.join(projectDir, "config.json");

  try {
    await fs.unlink(configPath);
  } catch {
    // Ignore if file doesn't exist
    return;
  }

  // Try to remove the workspaces subdirectory (only succeeds if empty)
  const workspacesDir = nodePath.join(projectDir, "workspaces");
  try {
    await fs.rm(workspacesDir);
  } catch {
    // ENOTEMPTY (workspaces exist) or ENOENT (doesn't exist) - that's fine
  }

  // Try to remove the project directory (only succeeds if empty)
  try {
    await fs.rm(projectDir);
  } catch {
    // ENOTEMPTY or ENOENT - that's fine
  }
}

// =============================================================================
// Module Factory
// =============================================================================

/**
 * Create a LocalProjectModule that owns project state and persistence for ALL projects.
 *
 * @param deps - FileSystemBoundary for persistence, GitWorktreeProvider for .git validation
 * @returns IntentModule with hook handlers for project:open, project:close, app:start
 */
export function createLocalProjectModule(deps: LocalProjectModuleDeps): IntentModule {
  const { projectsDir, fs, gitWorktreeProvider, dialogManager, gitClient } = deps;

  /** Internal state: all projects keyed by normalized path string. */
  const projects = new Map<string, LocalProject>();

  return {
    name: "local-project",
    hooks: {
      // resolve-project -> resolve (single registration replaces 5 per-operation hooks)
      [RESOLVE_PROJECT_OPERATION_ID]: {
        resolve: {
          handler: async (ctx: HookContext): Promise<ResolveProjectHookResult> => {
            const { projectPath } = ctx as ResolveProjectHookInput;
            const normalizedKey = new Path(projectPath).toString();
            const project = projects.get(normalizedKey);
            if (!project) return {};
            return { projectId: project.id, projectName: project.name };
          },
        },
      },

      [OPEN_PROJECT_OPERATION_ID]: {
        // prepare: offer git init for non-git directories
        prepare: {
          handler: async (ctx: HookContext): Promise<PrepareHookResult> => {
            const intent = ctx.intent as OpenProjectIntent;
            const { path, git } = intent.payload;

            // Self-select: only handle local paths
            if (git || !path) return {};

            // Already open — skip
            if (projects.has(path.toString())) return {};

            // Check if it's already a git repo
            let isRepo: boolean;
            try {
              isRepo = await gitClient.isRepositoryRoot(path);
            } catch {
              // Path doesn't exist or inaccessible — let resolve handle the error
              return {};
            }
            if (isRepo) return {};

            // Not a git repo — ask user
            const dialog = dialogManager.open({
              sections: [
                { type: "text", content: "Initialize Git Repository?", style: "heading" },
                { type: "text", content: path.toString(), style: "subtitle" },
              ],
              actions: [
                { id: "init", label: "Initialize" },
                { id: "cancel", label: "Cancel", variant: "secondary" },
              ],
              modal: true,
            });

            const event = await dialog.nextEvent();
            dialog.close();

            if (event.actionId !== "init") {
              return { canceled: true };
            }

            await gitClient.init(path, { initialCommit: "Initial commit" });
            return {};
          },
        },

        // resolve: validate .git exists for local paths
        resolve: {
          handler: async (ctx: HookContext): Promise<ResolveHookResult> => {
            const intent = ctx.intent as OpenProjectIntent;
            const { path, git } = intent.payload;

            // Self-select: only handle local paths (not git URLs)
            if (git || !path) {
              return {};
            }

            // Check persisted config for remoteUrl (restores icon on startup)
            const config = await getProjectConfig(fs, projectsDir, path.toString());
            const remoteUrl = config?.remoteUrl;

            // Already open — skip validation, signal short-circuit
            if (projects.has(path.toString())) {
              return {
                projectPath: path.toString(),
                alreadyOpen: true,
                ...(remoteUrl !== undefined && { remoteUrl }),
              };
            }

            await gitWorktreeProvider.validateRepository(path);

            return {
              projectPath: path.toString(),
              ...(remoteUrl !== undefined && { remoteUrl }),
            };
          },
        },

        // register: generate ID, persist, add to internal state (all projects)
        register: {
          handler: async (ctx: HookContext): Promise<RegisterHookResult> => {
            const { projectPath: projectPathStr, remoteUrl } = ctx as RegisterHookInput;

            const projectPath = new Path(projectPathStr);
            const normalizedKey = projectPath.toString();
            const projectId = generateProjectId(projectPathStr);

            // Already in state — return alreadyOpen without re-persisting
            if (projects.has(normalizedKey)) {
              return { projectId, name: projectPath.basename, alreadyOpen: true };
            }

            // Persist to store if new
            const existingConfig = await getProjectConfig(fs, projectsDir, projectPathStr);
            if (!existingConfig) {
              await saveProject(fs, projectsDir, projectPathStr, remoteUrl);
            }

            // Add to internal state
            projects.set(normalizedKey, {
              id: projectId,
              name: projectPath.basename,
              path: projectPath,
            });

            return { projectId, name: projectPath.basename };
          },
        },
      },

      [CLOSE_PROJECT_OPERATION_ID]: {
        // resolve: look up projectPath in config to get remoteUrl
        resolve: {
          handler: async (ctx: HookContext): Promise<CloseResolveHookResult> => {
            const intent = ctx.intent as CloseProjectIntent;
            const { projectPath } = intent.payload;

            // Look up config to get remoteUrl
            const config = await getProjectConfig(fs, projectsDir, projectPath);

            return {
              ...(config?.remoteUrl !== undefined && { remoteUrl: config.remoteUrl }),
            };
          },
        },

        // close: remove from internal state and config (all projects)
        close: {
          handler: async (ctx: HookContext): Promise<CloseHookResult> => {
            const { projectPath, removeLocalRepo, remoteUrl } = ctx as CloseHookInput;

            // Remove from internal state
            const normalizedKey = new Path(projectPath).toString();
            projects.delete(normalizedKey);

            if (removeLocalRepo && remoteUrl) {
              // Remote project with removeLocalRepo: force-delete the config dir
              const configDir = nodePath.join(projectsDir, projectDirName(projectPath));
              try {
                await fs.rm(configDir, { recursive: true, force: true });
              } catch {
                // Fail silently
              }
            } else {
              // Normal removal: remove config.json and empty dirs
              try {
                await removeProject(fs, projectsDir, projectPath);
              } catch {
                // Fail silently
              }
            }

            return { otherProjectsExist: projects.size > 0 };
          },
        },
      },

      [APP_READY_OPERATION_ID]: {
        // load-projects: load all saved project configs
        "load-projects": {
          handler: async (): Promise<LoadProjectsResult> => {
            const configs = await loadAllProjectConfigs(fs, projectsDir);
            return { projectPaths: configs.map((c) => c.path) };
          },
        },
      },

      // list-projects -> list-projects
      [LIST_PROJECTS_OPERATION_ID]: {
        "list-projects": {
          handler: async (): Promise<ListProjectsHookResult> => {
            const entries: ListProjectsHookEntry[] = [];
            for (const [key, project] of projects) {
              entries.push({
                projectId: project.id,
                name: project.name,
                path: key,
              });
            }
            return { projects: entries };
          },
        },
      },
    },
  };
}
