/* eslint-disable max-classes-per-file -- deep Effect v4 domain module: branded schemas, tagged errors, and context services */
import { Context, Effect, Layer, Schema } from "effect";

import { OrganizationId, ProjectId, TimestampMs } from "./signal.js";

export type { OrganizationId, ProjectId } from "./signal.js";

// ---------------------------------------------------------------------------
// Domain constants
// ---------------------------------------------------------------------------

export const MAX_CONTEXT_DOCUMENT_CHARACTERS = 200_000;
export const MAX_CONTEXT_BATCH_CHARACTERS = 600_000;

export const CONTEXT_KINDS = [
  "readme",
  "agents",
  "product",
  "business",
  "design",
  "tech",
] as const;

export type ContextKind = (typeof CONTEXT_KINDS)[number];

const CONTEXT_KIND_TO_PATH: Readonly<Record<ContextKind, string>> = {
  agents: "AGENTS.md",
  business: "business.md",
  design: "design.md",
  product: "product.md",
  readme: "README.md",
  tech: "tech.md",
};

const PATH_TO_CONTEXT_KIND: Readonly<Record<string, ContextKind>> =
  Object.fromEntries(
    Object.entries(CONTEXT_KIND_TO_PATH).map(([kind, path]) => [
      path,
      kind as ContextKind,
    ])
  );

export const contextPathForKind = (kind: ContextKind): string =>
  CONTEXT_KIND_TO_PATH[kind];

export const contextKindForPath = (path: string): ContextKind | null =>
  PATH_TO_CONTEXT_KIND[path] ?? null;

const ContextKindSchema = Schema.Literals([...CONTEXT_KINDS]);

const MeaningfulString = Schema.String.check(
  Schema.makeFilter((value) => value.trim().length > 0, {
    expected: "a non-empty string",
  })
);

// ---------------------------------------------------------------------------
// Shared data shapes
// ---------------------------------------------------------------------------

export const PreparedPublicGitSource = Schema.Struct({
  host: Schema.String,
  normalizedUrl: Schema.String,
  projectName: MeaningfulString,
  repositoryPath: MeaningfulString,
  url: Schema.String,
});
export type PreparedPublicGitSource = typeof PreparedPublicGitSource.Type;

export const RepositoryContextDocument = Schema.Struct({
  content: Schema.String,
  kind: ContextKindSchema,
  path: Schema.String,
});
export type RepositoryContextDocument = typeof RepositoryContextDocument.Type;

export const ProjectImportWarning = Schema.Struct({
  message: Schema.String,
  path: Schema.String,
});
export type ProjectImportWarning = typeof ProjectImportWarning.Type;

export const PublicGitImportResult = Schema.Struct({
  defaultBranch: Schema.UndefinedOr(Schema.String),
  documents: Schema.Array(RepositoryContextDocument),
  warnings: Schema.Array(ProjectImportWarning),
});
export type PublicGitImportResult = typeof PublicGitImportResult.Type;

export const PublicTextResult = Schema.Struct({
  content: Schema.String,
  finalUrl: Schema.String,
});
export type PublicTextResult = typeof PublicTextResult.Type;

export const ContextOrigin = Schema.Literals([
  "repository",
  "paste",
  "upload",
  "public-url",
]);
export type ContextOrigin = typeof ContextOrigin.Type;

export const ContextDocumentState = Schema.Struct({
  content: Schema.String,
  kind: ContextKindSchema,
  origin: ContextOrigin,
  path: Schema.String,
  revision: Schema.Number,
  sourceUrl: Schema.UndefinedOr(Schema.String),
});
export type ContextDocumentState = typeof ContextDocumentState.Type;

const RepositoryWrite = Schema.Struct({
  _tag: Schema.Literal("Repository"),
  content: Schema.String,
  kind: ContextKindSchema,
  sourceUrl: Schema.String,
});

const PublicTextUrlWrite = Schema.Struct({
  _tag: Schema.Literal("PublicTextUrl"),
  content: Schema.String,
  kind: ContextKindSchema,
  sourceUrl: Schema.String,
});

const UserTextWrite = Schema.Struct({
  _tag: Schema.Literal("UserText"),
  content: Schema.String,
  kind: ContextKindSchema,
  origin: Schema.Literals(["paste", "upload"]),
});

export const ContextWrite = Schema.Union([
  RepositoryWrite,
  PublicTextUrlWrite,
  UserTextWrite,
]);
export type ContextWrite = typeof ContextWrite.Type;

export const ProjectSourceView = Schema.Struct({
  createdAt: TimestampMs,
  defaultBranch: Schema.UndefinedOr(Schema.String),
  host: Schema.String,
  kind: Schema.Literal("git"),
  normalizedUrl: Schema.String,
  projectId: ProjectId,
  repositoryPath: Schema.String,
  updatedAt: TimestampMs,
  url: Schema.String,
});
export type ProjectSourceView = typeof ProjectSourceView.Type;

export const ProjectView = Schema.Struct({
  contextDocuments: Schema.Array(ContextDocumentState),
  createdAt: TimestampMs,
  id: ProjectId,
  name: Schema.String,
  organizationId: OrganizationId,
  sources: Schema.Array(ProjectSourceView),
  updatedAt: TimestampMs,
});
export type ProjectView = typeof ProjectView.Type;

export const ProjectImportOutcome = Schema.Struct({
  contextDocuments: Schema.Array(ContextDocumentState),
  createdAt: TimestampMs,
  id: ProjectId,
  name: Schema.String,
  organizationId: OrganizationId,
  source: ProjectSourceView,
  updatedAt: TimestampMs,
});
export type ProjectImportOutcome = typeof ProjectImportOutcome.Type;

// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------

export const PublicGitErrorReason = Schema.Literals([
  "InvalidUrl",
  "Unreachable",
  "InvalidResponse",
  "Cancelled",
]);
export type PublicGitErrorReason = typeof PublicGitErrorReason.Type;

export class PublicGitError extends Schema.TaggedErrorClass<PublicGitError>()(
  "PublicGitError",
  {
    message: Schema.String,
    reason: PublicGitErrorReason,
  }
) {}

export const ProjectStoreErrorReason = Schema.Literals([
  "NotFound",
  "Forbidden",
  "Conflict",
  "Persistence",
]);
export type ProjectStoreErrorReason = typeof ProjectStoreErrorReason.Type;

export class ProjectStoreError extends Schema.TaggedErrorClass<ProjectStoreError>()(
  "ProjectStoreError",
  {
    message: Schema.String,
    reason: ProjectStoreErrorReason,
  }
) {}

export const ProjectApplicationErrorReason = Schema.Literals([
  "Authentication",
  "InvalidPublicGitUrl",
  "PublicGitUnreachable",
  "InvalidRemoteResult",
  "InvalidContextInput",
  "ContextDocumentTooLarge",
  "ContextBatchTooLarge",
  "ProjectNotFound",
  "Persistence",
]);
export type ProjectApplicationErrorReason =
  typeof ProjectApplicationErrorReason.Type;

export class ProjectApplicationError extends Schema.TaggedErrorClass<ProjectApplicationError>()(
  "ProjectApplicationError",
  {
    message: Schema.String,
    reason: ProjectApplicationErrorReason,
  }
) {}

// ---------------------------------------------------------------------------
// Pure domain functions
// ---------------------------------------------------------------------------

const isWhitespaceOnly = (value: string): boolean => value.trim().length === 0;

const INVALID_GIT_URL = new ProjectApplicationError({
  message: "Use a public http(s) Git repository URL",
  reason: "InvalidPublicGitUrl",
});

const INVALID_REMOTE = new ProjectApplicationError({
  message: "Invalid remote result",
  reason: "InvalidRemoteResult",
});

export const preparePublicGitSource = (
  rawUrl: string
): Effect.Effect<PreparedPublicGitSource, ProjectApplicationError> =>
  Effect.gen(function* prepareSource() {
    let parsed: URL;
    try {
      parsed = new URL(rawUrl);
    } catch {
      return yield* Effect.fail(INVALID_GIT_URL);
    }

    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
      return yield* Effect.fail(INVALID_GIT_URL);
    }

    if (parsed.username || parsed.password || parsed.search || parsed.hash) {
      return yield* Effect.fail(INVALID_GIT_URL);
    }

    const host = parsed.hostname.toLowerCase();
    let repositoryPath = parsed.pathname;
    if (repositoryPath.startsWith("/")) {
      repositoryPath = repositoryPath.slice(1);
    }
    if (repositoryPath.endsWith("/")) {
      repositoryPath = repositoryPath.slice(0, -1);
    }
    if (repositoryPath.endsWith(".git")) {
      repositoryPath = repositoryPath.slice(0, -4);
    }

    if (repositoryPath.length === 0) {
      return yield* Effect.fail(INVALID_GIT_URL);
    }

    const segments = repositoryPath.split("/").filter((s) => s.length > 0);
    // eslint-disable-next-line unicorn/prefer-at -- backend Convex tsconfig targets pre-ES2022
    const lastSegment = segments[segments.length - 1];
    if (!lastSegment || isWhitespaceOnly(lastSegment)) {
      return yield* Effect.fail(INVALID_GIT_URL);
    }

    const projectName = lastSegment;
    const normalizedUrl = `${parsed.protocol}//${host}/${repositoryPath}`;

    return {
      host,
      normalizedUrl,
      projectName,
      repositoryPath,
      url: normalizedUrl,
    };
  });

export const decodePublicGitImportResult = (
  raw: unknown
): Effect.Effect<PublicGitImportResult, ProjectApplicationError> =>
  Effect.gen(function* decodeImport() {
    const result = yield* Schema.decodeUnknownEffect(PublicGitImportResult)(
      raw
    ).pipe(Effect.mapError(() => INVALID_REMOTE));

    const seenKinds = new Set<string>();
    const seenPaths = new Set<string>();
    let totalChars = 0;

    for (const doc of result.documents) {
      const canonicalPath = CONTEXT_KIND_TO_PATH[doc.kind];
      if (doc.path !== canonicalPath) {
        return yield* Effect.fail(INVALID_REMOTE);
      }
      if (seenKinds.has(doc.kind) || seenPaths.has(doc.path)) {
        return yield* Effect.fail(INVALID_REMOTE);
      }
      if (isWhitespaceOnly(doc.content)) {
        return yield* Effect.fail(INVALID_REMOTE);
      }
      if (doc.content.length > MAX_CONTEXT_DOCUMENT_CHARACTERS) {
        return yield* Effect.fail(
          new ProjectApplicationError({
            message: "Context document exceeds 200000 characters",
            reason: "ContextDocumentTooLarge",
          })
        );
      }
      seenKinds.add(doc.kind);
      seenPaths.add(doc.path);
      totalChars += doc.content.length;
    }

    if (totalChars > MAX_CONTEXT_BATCH_CHARACTERS) {
      return yield* Effect.fail(
        new ProjectApplicationError({
          message: "Context batch exceeds 600000 characters",
          reason: "ContextBatchTooLarge",
        })
      );
    }

    return result;
  });

export const decodePublicTextResult = (
  raw: unknown
): Effect.Effect<PublicTextResult, ProjectApplicationError> =>
  Effect.gen(function* decodeText() {
    const result = yield* Schema.decodeUnknownEffect(PublicTextResult)(
      raw
    ).pipe(Effect.mapError(() => INVALID_REMOTE));

    if (isWhitespaceOnly(result.content)) {
      return yield* Effect.fail(
        new ProjectApplicationError({
          message: "Context document cannot be empty",
          reason: "InvalidContextInput",
        })
      );
    }
    if (result.content.length > MAX_CONTEXT_DOCUMENT_CHARACTERS) {
      return yield* Effect.fail(
        new ProjectApplicationError({
          message: "Context document exceeds 200000 characters",
          reason: "ContextDocumentTooLarge",
        })
      );
    }
    if (isWhitespaceOnly(result.finalUrl)) {
      return yield* Effect.fail(INVALID_REMOTE);
    }

    return result;
  });

const resolveKindFromPath = (path: string): ContextKind => {
  const kind = contextKindForPath(path);
  if (kind === null) {
    throw new Error(`Unknown canonical context path: ${path}`);
  }
  return kind;
};

export const makeInitialContext = (
  projectName: string,
  repositoryUrl: string
): readonly ContextDocumentState[] => {
  const placeholder = (path: string): ContextDocumentState => ({
    content: `# ${path}\n\nContext not imported yet.\n`,
    kind: resolveKindFromPath(path),
    origin: "repository",
    path,
    revision: 1,
    sourceUrl: repositoryUrl,
  });

  return [
    {
      content: `# ${projectName}\n\nRepository: ${repositoryUrl}\n`,
      kind: "readme",
      origin: "repository",
      path: "README.md",
      revision: 1,
      sourceUrl: repositoryUrl,
    },
    placeholder("AGENTS.md"),
    placeholder("product.md"),
    placeholder("business.md"),
    placeholder("design.md"),
    placeholder("tech.md"),
  ];
};

const resolveWriteOrigin = (write: ContextWrite): ContextOrigin => {
  if (write._tag === "Repository") {
    return "repository";
  }
  if (write._tag === "PublicTextUrl") {
    return "public-url";
  }
  return write.origin;
};

export interface DecideContextWriteInput {
  readonly existing: ContextDocumentState | undefined;
  readonly write: ContextWrite;
}

export interface DecideContextWriteResult {
  readonly document: ContextDocumentState;
  readonly changed: boolean;
}

export const decideContextWrite = (
  input: DecideContextWriteInput
): Effect.Effect<DecideContextWriteResult, ProjectApplicationError> =>
  Effect.gen(function* decideWrite() {
    const { write } = input;
    const { content } = write;
    const { kind } = write;
    const path = contextPathForKind(kind);

    if (isWhitespaceOnly(content)) {
      return yield* Effect.fail(
        new ProjectApplicationError({
          message: "Context document cannot be empty",
          reason: "InvalidContextInput",
        })
      );
    }
    if (content.length > MAX_CONTEXT_DOCUMENT_CHARACTERS) {
      return yield* Effect.fail(
        new ProjectApplicationError({
          message: "Context document exceeds 200000 characters",
          reason: "ContextDocumentTooLarge",
        })
      );
    }

    const origin: ContextOrigin = resolveWriteOrigin(write);
    const sourceUrl: string | undefined =
      write._tag === "UserText" ? undefined : write.sourceUrl;

    const { existing } = input;
    if (
      existing &&
      existing.content === content &&
      existing.origin === origin &&
      existing.sourceUrl === sourceUrl
    ) {
      return { changed: false, document: existing };
    }

    const nextRevision = (existing?.revision ?? 0) + 1;
    return {
      changed: true,
      document: {
        content,
        kind,
        origin,
        path,
        revision: nextRevision,
        sourceUrl,
      },
    };
  });

// ---------------------------------------------------------------------------
// ProjectDomain service
// ---------------------------------------------------------------------------

interface ProjectDomainShape {
  readonly preparePublicGitSource: typeof preparePublicGitSource;
  readonly decodePublicGitImportResult: typeof decodePublicGitImportResult;
  readonly decodePublicTextResult: typeof decodePublicTextResult;
  readonly makeInitialContext: typeof makeInitialContext;
  readonly decideContextWrite: typeof decideContextWrite;
}

export class ProjectDomain extends Context.Service<
  ProjectDomain,
  ProjectDomainShape
>()("@code/primitives/project/ProjectDomain") {
  static readonly layer = Layer.succeed(
    ProjectDomain,
    ProjectDomain.of({
      decideContextWrite,
      decodePublicGitImportResult,
      decodePublicTextResult,
      makeInitialContext,
      preparePublicGitSource,
    })
  );
}

// ---------------------------------------------------------------------------
// PublicGit port
// ---------------------------------------------------------------------------

interface PublicGitShape {
  readonly inspect: (
    source: PreparedPublicGitSource
  ) => Effect.Effect<PublicGitImportResult, PublicGitError>;
  readonly fetchText: (
    url: string
  ) => Effect.Effect<PublicTextResult, PublicGitError>;
}

export class PublicGit extends Context.Service<PublicGit, PublicGitShape>()(
  "@code/primitives/project/PublicGit"
) {}

// ---------------------------------------------------------------------------
// ProjectStore port
// ---------------------------------------------------------------------------

interface ProjectStoreShape {
  readonly getCurrentOrganization: (
    userId: string
  ) => Effect.Effect<OrganizationId, ProjectStoreError>;
  readonly listProjects: (
    userId: string
  ) => Effect.Effect<readonly ProjectView[], ProjectStoreError>;
  readonly getProject: (
    userId: string,
    projectId: ProjectId
  ) => Effect.Effect<ProjectView | null, ProjectStoreError>;
  readonly persistPublicGitImport: (input: {
    readonly userId: string;
    readonly source: PreparedPublicGitSource;
    readonly remote: PublicGitImportResult;
  }) => Effect.Effect<ProjectImportOutcome, ProjectStoreError>;
  readonly putContext: (input: {
    readonly userId: string;
    readonly projectId: ProjectId;
    readonly write: ContextWrite;
  }) => Effect.Effect<{ readonly revision: number }, ProjectStoreError>;
}

export class ProjectStore extends Context.Service<
  ProjectStore,
  ProjectStoreShape
>()("@code/primitives/project/ProjectStore") {}

// ---------------------------------------------------------------------------
// Error mapping
// ---------------------------------------------------------------------------

const mapStoreError = (error: ProjectStoreError): ProjectApplicationError => {
  if (error.reason === "NotFound" || error.reason === "Forbidden") {
    return new ProjectApplicationError({
      message: "Project not found",
      reason: "ProjectNotFound",
    });
  }
  return new ProjectApplicationError({
    message: error.message,
    reason: "Persistence",
  });
};

const mapPublicGitError = (error: PublicGitError): ProjectApplicationError => {
  if (error.reason === "InvalidUrl") {
    return new ProjectApplicationError({
      message: error.message,
      reason: "InvalidPublicGitUrl",
    });
  }
  if (error.reason === "Unreachable" || error.reason === "Cancelled") {
    return new ProjectApplicationError({
      message: error.message,
      reason: "PublicGitUnreachable",
    });
  }
  return new ProjectApplicationError({
    message: error.message,
    reason: "InvalidRemoteResult",
  });
};

// ---------------------------------------------------------------------------
// ProjectApplication service
// ---------------------------------------------------------------------------

interface ProjectApplicationShape {
  readonly importPublicGit: (input: {
    readonly userId: string;
    readonly repositoryUrl: unknown;
  }) => Effect.Effect<ProjectImportOutcome, ProjectApplicationError>;
  readonly importPublicText: (input: {
    readonly userId: string;
    readonly projectId: ProjectId;
    readonly kind: ContextKind;
    readonly url: string;
  }) => Effect.Effect<{ readonly revision: number }, ProjectApplicationError>;
  readonly putContext: (input: {
    readonly userId: string;
    readonly projectId: ProjectId;
    readonly kind: ContextKind;
    readonly content: string;
    readonly origin: "paste" | "upload";
  }) => Effect.Effect<{ readonly revision: number }, ProjectApplicationError>;
  readonly listProjects: (input: {
    readonly userId: string;
  }) => Effect.Effect<readonly ProjectView[], ProjectApplicationError>;
  readonly getProject: (input: {
    readonly userId: string;
    readonly projectId: ProjectId;
  }) => Effect.Effect<ProjectView | null, ProjectApplicationError>;
}

export class ProjectApplication extends Context.Service<
  ProjectApplication,
  ProjectApplicationShape
>()("@code/primitives/project/ProjectApplication") {
  static readonly layer = Layer.effect(
    ProjectApplication,
    Effect.gen(function* layer() {
      const domain = yield* ProjectDomain;
      const publicGit = yield* PublicGit;
      const store = yield* ProjectStore;

      return ProjectApplication.of({
        getProject: Effect.fn("ProjectApplication.getProject")(function*  getProject({
          userId,
          projectId,
        }: {
          readonly userId: string;
          readonly projectId: ProjectId;
        }) {
          return yield* store
            .getProject(userId, projectId)
            .pipe(Effect.mapError(mapStoreError));
        }),
        importPublicGit: Effect.fn("ProjectApplication.importPublicGit")(
          function*  importPublicGit({
            userId,
            repositoryUrl,
          }: {
            readonly userId: string;
            readonly repositoryUrl: unknown;
          }) {
            if (typeof repositoryUrl !== "string") {
              return yield* Effect.fail(INVALID_GIT_URL);
            }

            const source = yield* domain.preparePublicGitSource(repositoryUrl);

            const rawRemote = yield* publicGit
              .inspect(source)
              .pipe(Effect.mapError(mapPublicGitError));

            const remote = yield* domain.decodePublicGitImportResult(rawRemote);

            return yield* store
              .persistPublicGitImport({ remote, source, userId })
              .pipe(Effect.mapError(mapStoreError));
          }
        ),
        importPublicText: Effect.fn("ProjectApplication.importPublicText")(
          function*  importPublicText({
            userId,
            projectId,
            kind,
            url,
          }: {
            readonly userId: string;
            readonly projectId: ProjectId;
            readonly kind: ContextKind;
            readonly url: string;
          }) {
            if (isWhitespaceOnly(url)) {
              return yield* Effect.fail(
                new ProjectApplicationError({
                  message: "Use a public text URL",
                  reason: "InvalidContextInput",
                })
              );
            }

            const rawText = yield* publicGit
              .fetchText(url)
              .pipe(Effect.mapError(mapPublicGitError));

            const text = yield* domain.decodePublicTextResult(rawText);

            const write: ContextWrite = {
              _tag: "PublicTextUrl",
              content: text.content,
              kind,
              sourceUrl: text.finalUrl,
            };

            return yield* store
              .putContext({ projectId, userId, write })
              .pipe(Effect.mapError(mapStoreError));
          }
        ),
        listProjects: Effect.fn("ProjectApplication.listProjects")(function*  listProjects({
          userId,
        }: {
          readonly userId: string;
        }) {
          return yield* store
            .listProjects(userId)
            .pipe(Effect.mapError(mapStoreError));
        }),
        putContext: Effect.fn("ProjectApplication.putContext")(function*  putContext({
          userId,
          projectId,
          kind,
          content,
          origin,
        }: {
          readonly userId: string;
          readonly projectId: ProjectId;
          readonly kind: ContextKind;
          readonly content: string;
          readonly origin: "paste" | "upload";
        }) {
          const write: ContextWrite = {
            _tag: "UserText",
            content,
            kind,
            origin,
          };
          return yield* store
            .putContext({ projectId, userId, write })
            .pipe(Effect.mapError(mapStoreError));
        }),
      });
    })
  );
}
