require 'puppet/indirector/code'
require 'puppet/file_bucket/file'
require 'puppet/util/checksums'
require 'fileutils'

module Puppet::FileBucketFile
  class File < Puppet::Indirector::Code
    include Puppet::Util::Checksums

    desc "Store files in a directory set based on their checksums."

    def initialize
      Puppet.settings.use(:filebucket)
    end

    def find( request )
      checksum, path = request_to_checksum_and_path( request )
      find_by_checksum( checksum, request.options )
    end

    def save( request )
      checksum, path = request_to_checksum_and_path( request )

      instance = request.instance
      instance.checksum = checksum if checksum
      instance.path = path if path

      save_to_disk(instance)
      instance.to_s
    end

    private

    def find_by_checksum( checksum, options )
      model.new( nil, :checksum => checksum ) do |bucket_file|
        bucket_file.bucket_path = options[:bucket_path]
        filename = contents_path_for( bucket_file )

        return nil if ! ::File.exist? filename

        begin
          contents = ::File.read filename
          Puppet.info "FileBucket read #{bucket_file.checksum}"
        rescue RuntimeError => e
          raise Puppet::Error, "file could not be read: #{e.message}"
        end

        if ::File.exist?(paths_path_for( bucket_file) )
          ::File.open(paths_path_for( bucket_file) ) do |f|
            bucket_file.paths = f.readlines.map { |l| l.chomp }
          end
        end

        bucket_file.contents = contents
      end
    end

    def save_to_disk( bucket_file )
      # If the file already exists, just return the md5 sum.
      if ::File.exist?(contents_path_for( bucket_file) )
        verify_identical_file!(bucket_file)
      else
        # Make the directories if necessary.
        unless ::File.directory?( path_for( bucket_file) )
          Puppet::Util.withumask(0007) do
            ::FileUtils.mkdir_p( path_for( bucket_file) )
          end
        end

        Puppet.info "FileBucket adding #{bucket_file.path} as #{bucket_file.checksum}"

        # Write the file to disk.
        Puppet::Util.withumask(0007) do
          ::File.open(contents_path_for(bucket_file), ::File::WRONLY|::File::CREAT, 0440) do |of|
            of.print bucket_file.contents
          end
        end
      end

      save_path_to_paths_file(bucket_file)
      bucket_file.checksum_data
    end

    def request_to_checksum_and_path( request )
      return [request.key, nil] if checksum?(request.key)

      checksum_type, checksum, path = request.key.split(/\//, 3)
      return(checksum_type.to_s == "" ? nil : [ "{#{checksum_type}}#{checksum}", path ])
    end

    def path_for(bucket_file, subfile = nil)
      bucket_path = bucket_file.bucket_path || Puppet[:bucketdir]
      digest      = bucket_file.checksum_data

      dir     = ::File.join(digest[0..7].split(""))
      basedir = ::File.join(bucket_path, dir, digest)

      return basedir unless subfile
      ::File.join(basedir, subfile)
    end

    def contents_path_for(bucket_file)
      path_for(bucket_file, "contents")
    end

    def paths_path_for(bucket_file)
      path_for(bucket_file, "paths")
    end

    def content_check?
      true
    end

    # If conflict_check is enabled, verify that the passed text is
    # the same as the text in our file.
    def verify_identical_file!(bucket_file)
      return unless content_check?
      disk_contents = ::File.read(contents_path_for(bucket_file))

      # If the contents don't match, then we've found a conflict.
      # Unlikely, but quite bad.
      if disk_contents != bucket_file.contents
        raise Puppet::FileBucket::BucketError, "Got passed new contents for sum #{bucket_file.checksum}", caller
      else
        Puppet.info "FileBucket got a duplicate file #{bucket_file.path} (#{bucket_file.checksum})"
      end
    end

    def save_path_to_paths_file(bucket_file)
      return unless bucket_file.path

      # check for dupes
      if ::File.exist?(paths_path_for( bucket_file) )
        ::File.open(paths_path_for( bucket_file) ) do |f|
          return if f.readlines.collect { |l| l.chomp }.include?(bucket_file.path)
        end
      end

      # if it's a new file, or if our path isn't in the file yet, add it
      ::File.open(paths_path_for(bucket_file), ::File::WRONLY|::File::CREAT|::File::APPEND) do |of|
        of.puts bucket_file.path
      end
    end

  end
end
