import { Effect, Console, pipe, Layer, Context } from 'effect'
import { Schema } from 'effect'
import * as os from 'node:os'
import * as path from 'node:path'
import * as fs from 'node:fs/promises'
import { spawn } from 'node:child_process'

// Error types with explicit interfaces
export interface WorktreeCreationErrorFields {
  readonly message: string
  readonly cause?: unknown
}

const WorktreeCreationErrorSchema = Schema.TaggedError<WorktreeCreationErrorFields>()(
  'WorktreeCreationError',
  {
    message: Schema.String,
    cause: Schema.optional(Schema.Unknown),
  },
) as unknown

export class WorktreeCreationError
  extends (WorktreeCreationErrorSchema as new (
    args: WorktreeCreationErrorFields,
  ) => WorktreeCreationErrorFields & Error & { readonly _tag: 'WorktreeCreationError' })
  implements Error
{
  readonly name = 'WorktreeCreationError'
}

export interface PatchsetFetchErrorFields {
  readonly message: string
  readonly cause?: unknown
}

const PatchsetFetchErrorSchema = Schema.TaggedError<PatchsetFetchErrorFields>()(
  'PatchsetFetchError',
  {
    message: Schema.String,
    cause: Schema.optional(Schema.Unknown),
  },
) as unknown

export class PatchsetFetchError
  extends (PatchsetFetchErrorSchema as new (
    args: PatchsetFetchErrorFields,
  ) => PatchsetFetchErrorFields & Error & { readonly _tag: 'PatchsetFetchError' })
  implements Error
{
  readonly name = 'PatchsetFetchError'
}

export interface DirtyRepoErrorFields {
  readonly message: string
}

const DirtyRepoErrorSchema = Schema.TaggedError<DirtyRepoErrorFields>()('DirtyRepoError', {
  message: Schema.String,
}) as unknown

export class DirtyRepoError
  extends (DirtyRepoErrorSchema as new (
    args: DirtyRepoErrorFields,
  ) => DirtyRepoErrorFields & Error & { readonly _tag: 'DirtyRepoError' })
  implements Error
{
  readonly name = 'DirtyRepoError'
}

export interface NotGitRepoErrorFields {
  readonly message: string
}

const NotGitRepoErrorSchema = Schema.TaggedError<NotGitRepoErrorFields>()('NotGitRepoError', {
  message: Schema.String,
}) as unknown

export class NotGitRepoError
  extends (NotGitRepoErrorSchema as new (
    args: NotGitRepoErrorFields,
  ) => NotGitRepoErrorFields & Error & { readonly _tag: 'NotGitRepoError' })
  implements Error
{
  readonly name = 'NotGitRepoError'
}

export type GitWorktreeError =
  | WorktreeCreationError
  | PatchsetFetchError
  | DirtyRepoError
  | NotGitRepoError

// Worktree info
export interface WorktreeInfo {
  path: string
  changeId: string
  originalCwd: string
  timestamp: number
  pid: number
}

// Git command runner with Effect
const runGitCommand = (
  args: string[],
  options: { cwd?: string } = {},
): Effect.Effect<string, GitWorktreeError, never> =>
  Effect.async<string, GitWorktreeError, never>((resume) => {
    const child = spawn('git', args, {
      cwd: options.cwd || process.cwd(),
      stdio: ['ignore', 'pipe', 'pipe'],
    })

    let stdout = ''
    let stderr = ''

    child.stdout?.on('data', (data) => {
      stdout += data.toString()
    })

    child.stderr?.on('data', (data) => {
      stderr += data.toString()
    })

    child.on('close', (code) => {
      if (code === 0) {
        resume(Effect.succeed(stdout.trim()))
      } else {
        const errorMessage = `Git command failed: git ${args.join(' ')}\nStderr: ${stderr}`

        // Classify error based on command and output
        if (args[0] === 'worktree' && args[1] === 'add') {
          resume(Effect.fail(new WorktreeCreationError({ message: errorMessage })))
        } else if (args[0] === 'fetch' || args[0] === 'checkout') {
          resume(Effect.fail(new PatchsetFetchError({ message: errorMessage })))
        } else {
          resume(Effect.fail(new WorktreeCreationError({ message: errorMessage })))
        }
      }
    })

    child.on('error', (error) => {
      resume(
        Effect.fail(
          new WorktreeCreationError({
            message: `Failed to spawn git: ${error.message}`,
            cause: error,
          }),
        ),
      )
    })
  })

// Check if current directory is a git repository
const validateGitRepo = (): Effect.Effect<void, NotGitRepoError, never> =>
  pipe(
    runGitCommand(['rev-parse', '--git-dir']),
    Effect.mapError(
      () => new NotGitRepoError({ message: 'Current directory is not a git repository' }),
    ),
    Effect.map(() => undefined),
  )

// Generate unique worktree path
const generateWorktreePath = (changeId: string): string => {
  const timestamp = Date.now()
  const pid = process.pid
  const uniqueId = `${changeId}-${timestamp}-${pid}`
  return path.join(os.homedir(), '.ger', 'worktrees', uniqueId)
}

// Ensure .ger directory exists
const ensureGerDirectory = (): Effect.Effect<void, never, never> =>
  Effect.tryPromise({
    try: async () => {
      const gerDir = path.join(os.homedir(), '.ger', 'worktrees')
      await fs.mkdir(gerDir, { recursive: true })
    },
    catch: () => undefined, // Ignore errors, will fail later if directory can't be created
  }).pipe(Effect.catchAll(() => Effect.succeed(undefined)))

// Build Gerrit refspec for change
const buildRefspec = (changeNumber: string, patchsetNumber: number = 1): string => {
  // Extract change number from changeId if it contains non-numeric characters
  const numericChangeNumber = changeNumber.replace(/\D/g, '')
  return `refs/changes/${numericChangeNumber.slice(-2)}/${numericChangeNumber}/${patchsetNumber}`
}

// Get the current HEAD commit hash to avoid branch conflicts
const getCurrentCommit = (): Effect.Effect<string, GitWorktreeError, never> =>
  pipe(
    runGitCommand(['rev-parse', 'HEAD']),
    Effect.map((output) => output.trim()),
    Effect.catchAll(() =>
      // Fallback: try to get commit from default branch
      pipe(
        runGitCommand(['rev-parse', 'origin/main']),
        Effect.catchAll(() => runGitCommand(['rev-parse', 'origin/master'])),
        Effect.catchAll(() => Effect.succeed('HEAD')),
      ),
    ),
  )

// Get latest patchset number for a change
const getLatestPatchsetNumber = (
  changeId: string,
): Effect.Effect<number, PatchsetFetchError, never> =>
  pipe(
    runGitCommand(['ls-remote', 'origin', `refs/changes/*/${changeId.replace(/\D/g, '')}/*`]),
    Effect.mapError(
      (error) =>
        new PatchsetFetchError({ message: `Failed to get patchset info: ${error.message}` }),
    ),
    Effect.map((output) => {
      const lines = output.split('\n').filter((line) => line.trim())
      if (lines.length === 0) {
        return 1 // Default to patchset 1 if no refs found
      }

      // Extract patchset numbers and return the highest
      const patchsetNumbers = lines
        .map((line) => {
          const match = line.match(/refs\/changes\/\d+\/\d+\/(\d+)$/)
          return match ? parseInt(match[1], 10) : 0
        })
        .filter((num) => num > 0)

      return patchsetNumbers.length > 0 ? Math.max(...patchsetNumbers) : 1
    }),
  )

// GitWorktreeService implementation
export interface GitWorktreeServiceImpl {
  validatePreconditions: () => Effect.Effect<void, GitWorktreeError, never>
  createWorktree: (changeId: string) => Effect.Effect<WorktreeInfo, GitWorktreeError, never>
  fetchAndCheckoutPatchset: (
    worktreeInfo: WorktreeInfo,
  ) => Effect.Effect<void, GitWorktreeError, never>
  cleanup: (worktreeInfo: WorktreeInfo) => Effect.Effect<void, never, never>
  getChangedFiles: () => Effect.Effect<string[], GitWorktreeError, never>
}

const GitWorktreeServiceImplLive: GitWorktreeServiceImpl = {
  validatePreconditions: () =>
    Effect.gen(function* () {
      yield* validateGitRepo()
      yield* Console.log('✓ Git repository validation passed')
    }),

  createWorktree: (changeId: string) =>
    Effect.gen(function* () {
      yield* Console.log(`→ Creating worktree for change ${changeId}...`)

      // Get current commit hash to avoid branch conflicts
      const currentCommit = yield* getCurrentCommit()
      yield* Console.log(`→ Using base commit: ${currentCommit.substring(0, 7)}`)

      // Ensure .ger directory exists
      yield* ensureGerDirectory()

      // Generate unique path
      const worktreePath = generateWorktreePath(changeId)
      const originalCwd = process.cwd()

      // Create worktree using commit hash (no branch conflicts)
      yield* runGitCommand(['worktree', 'add', '--detach', worktreePath, currentCommit])

      const worktreeInfo: WorktreeInfo = {
        path: worktreePath,
        changeId,
        originalCwd,
        timestamp: Date.now(),
        pid: process.pid,
      }

      yield* Console.log(`✓ Worktree created at ${worktreePath}`)
      return worktreeInfo
    }),

  fetchAndCheckoutPatchset: (worktreeInfo: WorktreeInfo) =>
    Effect.gen(function* () {
      yield* Console.log(`→ Fetching and checking out patchset for ${worktreeInfo.changeId}...`)

      // Get latest patchset number
      const patchsetNumber = yield* getLatestPatchsetNumber(worktreeInfo.changeId)
      const refspec = buildRefspec(worktreeInfo.changeId, patchsetNumber)

      yield* Console.log(`→ Using refspec: ${refspec}`)

      // Fetch the change
      yield* runGitCommand(['fetch', 'origin', refspec], { cwd: worktreeInfo.path })

      // Checkout FETCH_HEAD
      yield* runGitCommand(['checkout', 'FETCH_HEAD'], { cwd: worktreeInfo.path })

      yield* Console.log(`✓ Checked out patchset ${patchsetNumber} for ${worktreeInfo.changeId}`)
    }),

  cleanup: (worktreeInfo: WorktreeInfo) =>
    Effect.gen(function* () {
      yield* Console.log(`→ Cleaning up worktree for ${worktreeInfo.changeId}...`)

      // Always restore original working directory first
      try {
        process.chdir(worktreeInfo.originalCwd)
      } catch (error) {
        yield* Console.warn(`Warning: Could not restore original directory: ${error}`)
      }

      // Attempt to remove worktree (don't fail if this doesn't work)
      yield* pipe(
        runGitCommand(['worktree', 'remove', '--force', worktreeInfo.path]),
        Effect.catchAll((error) =>
          Effect.gen(function* () {
            yield* Console.warn(`Warning: Could not remove worktree: ${error.message}`)
            yield* Console.warn(`Manual cleanup may be required: ${worktreeInfo.path}`)
          }),
        ),
      )

      yield* Console.log(`✓ Cleanup completed for ${worktreeInfo.changeId}`)
    }),

  getChangedFiles: () =>
    Effect.gen(function* () {
      // Get list of changed files in current worktree
      const output = yield* runGitCommand(['diff', '--name-only', 'HEAD~1'])
      const files = output.split('\n').filter((file) => file.trim())
      return files
    }),
}

// Export service tag for dependency injection with explicit type
export const GitWorktreeService: Context.Tag<GitWorktreeServiceImpl, GitWorktreeServiceImpl> =
  Context.GenericTag<GitWorktreeServiceImpl>('GitWorktreeService')

export type GitWorktreeService = Context.Tag.Identifier<typeof GitWorktreeService>

// Export service layer with explicit type
export const GitWorktreeServiceLive: Layer.Layer<GitWorktreeServiceImpl> = Layer.succeed(
  GitWorktreeService,
  GitWorktreeServiceImplLive,
)
