import {
  CONTEXT_KINDS,
  contextPathForKind,
  decideContextWrite,
  makeInitialContext,
  type ContextDocumentState,
  type ContextKind,
  type ContextWrite,
  type PreparedPublicGitSource,
  type ProjectImportOutcome,
  type ProjectView,
  type PublicGitImportResult,
} from "@code/primitives/project";

import type { Doc, Id } from "./_generated/dataModel";
import type { ActionCtx, MutationCtx, QueryCtx } from "./_generated/server";
import { requireCurrentOrganization, requireProjectMember } from "./authz";

// ---------------------------------------------------------------------------
// View mappers
// ---------------------------------------------------------------------------

interface SourceRow extends Doc<"projectSources"> {}

const toContextDocumentState = (
  doc: Doc<"projectContextDocuments">
): ContextDocumentState => ({
  content: doc.content,
  kind: doc.kind,
  origin: doc.origin,
  path: doc.path,
  revision: doc.revision,
  sourceUrl: doc.sourceUrl ?? undefined,
});

const toProjectView = async (
  ctx: QueryCtx,
  project: Doc<"projects">
): Promise<ProjectView> => {
  const [sources, contextDocs] = await Promise.all([
    ctx.db
      .query("projectSources")
      .withIndex("by_project", (q) => q.eq("projectId", project._id))
      .collect(),
    ctx.db
      .query("projectContextDocuments")
      .withIndex("by_project", (q) => q.eq("projectId", project._id))
      .collect(),
  ]);

  return {
    contextDocuments: contextDocs.map(toContextDocumentState),
    createdAt: project.createdAt as never,
    id: project._id as never,
    name: project.name,
    organizationId: project.organizationId as never,
    sources: sources.map((s) => ({
      createdAt: s.createdAt as never,
      defaultBranch: s.defaultBranch,
      host: s.host,
      kind: "git" as const,
      normalizedUrl: s.normalizedUrl,
      projectId: s.projectId as never,
      repositoryPath: s.repositoryPath,
      updatedAt: s.updatedAt as never,
      url: s.url,
    })),
    updatedAt: project.updatedAt as never,
  };
};

const toImportOutcome = async (
  ctx: QueryCtx,
  project: Doc<"projects">,
  source: SourceRow
): Promise<ProjectImportOutcome> => {
  const contextDocs = await ctx.db
    .query("projectContextDocuments")
    .withIndex("by_project", (q) => q.eq("projectId", project._id))
    .collect();

  return {
    contextDocuments: contextDocs.map(toContextDocumentState),
    createdAt: project.createdAt as never,
    id: project._id as never,
    name: project.name,
    organizationId: project.organizationId as never,
    source: {
      createdAt: source.createdAt as never,
      defaultBranch: source.defaultBranch,
      host: source.host,
      kind: "git" as const,
      normalizedUrl: source.normalizedUrl,
      projectId: source.projectId as never,
      repositoryPath: source.repositoryPath,
      updatedAt: source.updatedAt as never,
      url: source.url,
    },
    updatedAt: project.updatedAt as never,
  };
};

// ---------------------------------------------------------------------------
// Query store — read-only ctx.db access
// ---------------------------------------------------------------------------

export const makeProjectQueryStore = (ctx: QueryCtx) => ({
  listProjects: async (userId: string): Promise<ProjectView[]> => {
    const { organizationId } = await resolveOrgForUser(ctx, userId);
    const projects = await ctx.db
      .query("projects")
      .withIndex("by_organization_and_createdAt", (q) =>
        q.eq("organizationId", organizationId)
      )
      .order("desc")
      .take(50);
    return Promise.all(projects.map((p) => toProjectView(ctx, p)));
  },

  getProject: async (
    userId: string,
    projectId: Id<"projects">
  ): Promise<ProjectView | null> => {
    const { organizationId } = await resolveOrgForUser(ctx, userId);
    const project = await ctx.db.get(projectId);
    if (!project || project.organizationId !== organizationId) {
      return null;
    }
    return toProjectView(ctx, project);
  },
});

// ---------------------------------------------------------------------------
// Mutation store — transactional ctx.db writes
// ---------------------------------------------------------------------------

export const makeProjectMutationStore = (ctx: MutationCtx) => ({
  putContext: async (
    userId: string,
    projectId: Id<"projects">,
    write: ContextWrite
  ): Promise<{ revision: number }> => {
    const { organizationId } = await resolveProjectOrg(ctx, projectId, userId);
    const path = contextPathForKind(write.kind);
    const existing = await ctx.db
      .query("projectContextDocuments")
      .withIndex("by_project_and_path", (q) =>
        q.eq("projectId", projectId).eq("path", path)
      )
      .unique();

    const result = await Effect.runPromise(
      decideContextWrite({
        existing: existing ? toContextDocumentState(existing) : undefined,
        write,
      })
    );
    const now = Date.now();

    if (!result.changed) {
      return { revision: result.document.revision };
    }

    if (existing) {
      await ctx.db.patch(existing._id, {
        content: result.document.content,
        origin: result.document.origin,
        revision: result.document.revision,
        sourceUrl: result.document.sourceUrl,
        updatedAt: now,
      });
    } else {
      await ctx.db.insert("projectContextDocuments", {
        content: result.document.content,
        createdAt: now,
        kind: result.document.kind,
        organizationId,
        origin: result.document.origin,
        path: result.document.path,
        projectId,
        revision: result.document.revision,
        sourceUrl: result.document.sourceUrl,
        updatedAt: now,
      });
    }

    return { revision: result.document.revision };
  },
});

// ---------------------------------------------------------------------------
// Action store — calls narrowly scoped internal mutations/queries
// ---------------------------------------------------------------------------

export const makeProjectActionStore = (_ctx: ActionCtx) => ({
  getCurrentOrganization: async (userId: string) => {
    return userId;
  },

  listProjects: async (userId: string) => {
    return userId;
  },

  getProject: async (userId: string, _projectId: Id<"projects">) => {
    return userId;
  },

  persistPublicGitImport: async (
    _userId: string,
    _source: PreparedPublicGitSource,
    _remote: PublicGitImportResult
  ): Promise<ProjectImportOutcome> => {
    throw new Error(
      "persistPublicGitImport must be called via internal mutation"
    );
  },

  putContext: async (
    _userId: string,
    _projectId: Id<"projects">,
    _write: ContextWrite
  ): Promise<{ revision: number }> => {
    throw new Error("putContext must be called via internal mutation");
  },
});

// We need Effect for decideContextWrite at runtime.
import { Effect } from "effect";

// ---------------------------------------------------------------------------
// Internal mutation store — the actual transactional persistence
// ---------------------------------------------------------------------------

export const persistPublicGitImportTransaction = async (
  ctx: MutationCtx,
  userId: string,
  source: PreparedPublicGitSource,
  remote: PublicGitImportResult
): Promise<ProjectImportOutcome> => {
  const { organizationId } = await resolveOrgForUser(ctx, userId);

  // Idempotency: lookup by normalizedUrl within the organization
  const existingSource = await ctx.db
    .query("projectSources")
    .withIndex("by_organization_and_normalizedUrl", (q) =>
      q
        .eq("organizationId", organizationId)
        .eq("normalizedUrl", source.normalizedUrl)
    )
    .unique();

  const now = Date.now();

  if (existingSource) {
    const project = await ctx.db.get(existingSource.projectId);
    if (!project) {
      throw new Error("Project not found for existing source");
    }
    // Update source metadata
    await ctx.db.patch(existingSource._id, {
      defaultBranch: remote.defaultBranch,
      updatedAt: now,
    });
    // Apply remote documents as repository writes
    for (const doc of remote.documents) {
      await applyRepositoryWrite(
        ctx,
        project._id,
        organizationId,
        doc,
        source.url,
        now
      );
    }
    await ctx.db.patch(project._id, { updatedAt: now });
    const updatedSource = await ctx.db.get(existingSource._id);
    return toImportOutcome(ctx, project, updatedSource ?? existingSource);
  }

  // Create new project
  const projectId = await ctx.db.insert("projects", {
    createdAt: now,
    name: source.projectName,
    organizationId,
    updatedAt: now,
  });

  // Create source
  const sourceId = await ctx.db.insert("projectSources", {
    createdAt: now,
    defaultBranch: remote.defaultBranch,
    host: source.host,
    kind: "git",
    normalizedUrl: source.normalizedUrl,
    organizationId,
    projectId,
    repositoryPath: source.repositoryPath,
    updatedAt: now,
    url: source.url,
  });

  // Seed six canonical context documents
  const seeds = makeInitialContext(source.projectName, source.url);
  for (const seed of seeds) {
    await ctx.db.insert("projectContextDocuments", {
      content: seed.content,
      createdAt: now,
      kind: seed.kind,
      organizationId,
      origin: seed.origin,
      path: seed.path,
      projectId,
      revision: seed.revision,
      sourceUrl: seed.sourceUrl,
      updatedAt: now,
    });
  }

  // Apply remote documents (overrides seeds)
  for (const doc of remote.documents) {
    await applyRepositoryWrite(
      ctx,
      projectId,
      organizationId,
      doc,
      source.url,
      now
    );
  }

  const createdProject = await ctx.db.get(projectId);
  const createdSource = await ctx.db.get(sourceId);
  if (!createdProject || !createdSource) {
    throw new Error("Failed to read created project");
  }
  return toImportOutcome(ctx, createdProject, createdSource);
};

const applyRepositoryWrite = async (
  ctx: MutationCtx,
  projectId: Id<"projects">,
  organizationId: Id<"organizations">,
  doc: { kind: ContextKind; path: string; content: string },
  sourceUrl: string,
  now: number
) => {
  const path = contextPathForKind(doc.kind);
  const existing = await ctx.db
    .query("projectContextDocuments")
    .withIndex("by_project_and_path", (q) =>
      q.eq("projectId", projectId).eq("path", path)
    )
    .unique();

  const result = await Effect.runPromise(
    decideContextWrite({
      existing: existing ? toContextDocumentState(existing) : undefined,
      write: {
        _tag: "Repository" as const,
        content: doc.content,
        kind: doc.kind,
        sourceUrl,
      },
    })
  );

  if (!result.changed) return;

  if (existing) {
    await ctx.db.patch(existing._id, {
      content: result.document.content,
      origin: result.document.origin,
      revision: result.document.revision,
      sourceUrl: result.document.sourceUrl,
      updatedAt: now,
    });
  } else {
    await ctx.db.insert("projectContextDocuments", {
      content: result.document.content,
      createdAt: now,
      kind: result.document.kind,
      organizationId,
      origin: result.document.origin,
      path: result.document.path,
      projectId,
      revision: result.document.revision,
      sourceUrl: result.document.sourceUrl,
      updatedAt: now,
    });
  }
};

// ---------------------------------------------------------------------------
// Organization resolvers
// ---------------------------------------------------------------------------

const resolveOrgForUser = async (
  ctx: QueryCtx | MutationCtx,
  userId: string
): Promise<{ organizationId: Id<"organizations"> }> => {
  const organization = await ctx.db
    .query("organizations")
    .withIndex("by_createdBy_and_kind", (q) =>
      q.eq("createdBy", userId).eq("kind", "personal")
    )
    .unique();
  if (!organization) {
    throw new Error("Organization not found");
  }
  return { organizationId: organization._id };
};

const resolveProjectOrg = async (
  ctx: QueryCtx | MutationCtx,
  projectId: Id<"projects">,
  userId: string
): Promise<{ organizationId: Id<"organizations"> }> => {
  const project = await ctx.db.get(projectId);
  if (!project) {
    throw new Error("Project not found");
  }
  const { organizationId } = await resolveOrgForUser(ctx, userId);
  if (project.organizationId !== organizationId) {
    throw new Error("Project not found");
  }
  return { organizationId };
};

// Re-export for convenience
export { CONTEXT_KINDS, requireCurrentOrganization, requireProjectMember };
