require 'yaml'
require 'json'
require 'citeproc'
require 'csl/styles'
require 'bibtex'
require 'cgi'

module EBSCO

  module EDS

    # A single search result
    class Record

      # Raw record as returned by the \EDS API via search or retrieve
      attr_reader :record

      # Creates a search or retrieval result record
      def initialize(results_record)

        if results_record.key? 'Record'
          @record = results_record['Record'] # single record returned by retrieve api
        else
          @record = results_record # set of records returned by search api
        end

        @items = @record.fetch('Items', {})

        @bib_entity = @record.fetch('RecordInfo', {})
                          .fetch('BibRecord', {})
                          .fetch('BibEntity', {})

        @bib_relationships = @record.fetch('RecordInfo', {})
                                 .fetch('BibRecord', {})
                                 .fetch('BibRelationships', {})

        @bib_part = @record.fetch('RecordInfo', {})
                        .fetch('BibRecord', {})
                        .fetch('BibRelationships', {})
                        .fetch('IsPartOfRelationships', {})[0]

        @bibtex = BibTeX::Entry.new
      end

      # \Options hash containing accession number and database ID. This can be passed to the retrieve method.
      def retrieve_options
        options = {}
        options['an'] = accession_number
        options['dbid'] = database_id
        options
      end

      # The accession number.
      def accession_number
        header_an
      end

      # The database ID.
      def database_id
        header_db_id
      end

      # The database name or label.
      def database_name
        header_db_label
      end

      # The access level.
      def access_level
        header_access_level
      end

      # The search relevancy score.
      def relevancy_score
        header_score
      end

      # The title.
      def title
        # _retval = get_item_data_by_name('Title') || bib_title
        _retval = bib_title || get_item_data_by_name('Title')
        # TODO: make this configurable
        if _retval.nil?
          _retval = 'This title is unavailable for guests, please login to see more information.'
        end
        CGI.unescapeHTML(_retval)
      end

      # The source title (e.g., Journal)
      def source_title
        _retval = bib_source_title || get_item_data_by_name('TitleSource')
        _reval = nil? if _retval == title # suppress if it's identical to title
        _retval.nil?? nil : CGI.unescapeHTML(_retval)
      end

      # Other alternative titles.
      def other_titles
        _retval = get_item_data_by_name('TitleAlt')
        _retval.nil?? nil : CGI.unescapeHTML(_retval)
      end

      # The abstract
      def abstract
        _retval = get_item_data_by_name('Abstract')
        _retval.nil?? nil : CGI.unescapeHTML(_retval)
      end

      # The list of authors
      def authors
        bib_authors || get_item_data_by_name('Author')
      end

      # The author affiliations
      def author_affiliations
        get_item_data_by_name('AffiliationAuthor')
      end

      # The list of subject terms.
      def subjects
        bib_subjects || get_item_data_by_name('Subject')
      end

      # The list of geographic subjects
      def subjects_geographic
        get_item_data_by_name('SubjectGeographic')
      end

      # The list of person subjects
      def subjects_person
        get_item_data_by_name('SubjectPerson')
      end

      # Author supplied keywords
      def author_supplied_keywords
        get_item_data_by_label('Author-Supplied Keywords')
      end

      # Notes
      def notes
        _retval = get_item_data_by_name('Note')
        _retval.nil?? nil : CGI.unescapeHTML(_retval)
      end

      # Languages
      def languages
        get_item_data_by_name('Language') || bib_languages
      end

      # Total number of pages.
      def page_count
        bib_page_count
      end

      # Starting page number.
      def page_start
        bib_page_start
      end

      # Physical description.
      def physical_description
        get_item_data_by_name('PhysDesc')
      end

      # Publication type.
      def publication_type
        header_publication_type || get_item_data_by_name('TypePub')
      end

      # Publication type ID.
      def publication_type_id
        header_publication_type_id
      end

      # Publication date.
      def publication_date
        bib_publication_date || get_item_data_by_name('DatePub')
      end

      # Publication year.
      def publication_year
        bib_publication_year || get_item_data_by_name('DatePub')
      end

      # Publisher information.
      def publisher_info
        get_item_data_by_label('Publication Information')
      end

      # Document type.
      def document_type
        get_item_data_by_name('TypeDocument')
      end

      # DOI identifier.
      def doi
        get_item_data_by_name('DOI') || bib_doi
      end

      # OCLC identifier.
      def oclc
        get_item_data_by_label('OCLC')
      end

      #  Prind ISSN
      def issn_print
        get_item_data_by_name('ISSN') || bib_issn_print
      end

      # List of ISSNs
      def issns
        bib_issns
      end

      # List of ISBNs
      def isbns
        bib_isbns || item_related_isbns
      end

      # Print ISBN
      def isbn_print
        bib_isbn_print
      end

      # Electronic ISBN
      def isbn_electronic
        bib_isbn_electronic
      end

      # Series information.
      def series
        get_item_data_by_name('SeriesInfo')
      end

      # Volume
      def volume
        bib_volume
      end

      # Issue
      def issue
        bib_issue
      end

      # Cover images
      def covers
        images
      end

      # Cover image - thumbnail size link
      def cover_thumb_url
        if images('thumb').any?
          images('thumb').first[:src]
        else
          nil
        end
      end

      # Cover image - medium size link
      def cover_medium_url
        if images('medium').any?
          images('medium').first[:src]
        else
          nil
        end
      end

      # Word count for fulltext.
      def fulltext_word_count
        get_item_data_by_name('FullTextWordCount').to_i
      end

      # --
      # ====================================================================================
      # GENERAL: ResultId, PLink, ImageInfo, CustomLinks, FullText
      # ====================================================================================
      # ++

      # Result ID.
      def result_id
        @record['ResultId']
      end

      # EBSCO's persistent link.
      def plink
        @record['PLink']
      end

      # Fulltext.
      def html_fulltext
        if @record.fetch('FullText',{}).fetch('Text',{}).fetch('Availability',0) == '1'
          @record.fetch('FullText',{}).fetch('Text',{})['Value']
        else
          nil
        end
      end

      # List of cover images.
      def images (size_requested = 'all')
        returned_images = []
        images = @record.fetch('ImageInfo', {})
        if images.count > 0
          images.each do |image|
            if size_requested == image['Size'] || size_requested == 'all'
              returned_images.push({size: image['Size'], src: image['Target']})
            end
          end
        end
        returned_images
      end

      # --
      # ====================================================================================
      # LINK HELPERS
      # ====================================================================================
      # ++

      # A list of all available links.
      def all_links
        fulltext_links + non_fulltext_links
      end

      # The first fulltext link.
      def fulltext_link
        fulltext_links.first || {}
      end

      # All available fulltext links.
      def fulltext_links

        links = []

        ebscolinks = @record.fetch('FullText',{}).fetch('Links',{})
        if ebscolinks.count > 0
          ebscolinks.each do |ebscolink|
            if ebscolink['Type'] == 'pdflink'
              link_label = 'PDF Full Text'
              link_icon = 'PDF Full Text Icon'
              link_url = ebscolink['Url'] || 'detail'
              links.push({url: link_url, label: link_label, icon: link_icon, type: 'pdf'})
            end
          end
        end

        # commenting out for now, not sure how 'detail' urls are useful in a blacklight context?
        # htmlfulltextcheck = @record.fetch('FullText',{}).fetch('Text',{}).fetch('Availability',{})
        # if htmlfulltextcheck == '1'
        #   link_url = 'detail'
        #   link_label = 'Full Text in Browser'
        #   link_icon = 'Full Text in Browser Icon'
        #   links.push({url: link_url, label: link_label, icon: link_icon, type: 'html'})
        # end

        if ebscolinks.count > 0
          ebscolinks.each do |ebscolink|
            if ebscolink['Type'] == 'ebook-pdf'
              link_label = 'PDF eBook Full Text'
              link_icon = 'PDF eBook Full Text Icon'
              link_url = ebscolink['Url'] || 'detail'
              links.push({url: link_url, label: link_label, icon: link_icon, type: 'ebook-pdf'})
            end
          end
        end

        if ebscolinks.count > 0
          ebscolinks.each do |ebscolink|
            if ebscolink['Type'] == 'ebook-epub'
              link_label = 'ePub eBook Full Text'
              link_icon = 'ePub eBook Full Text Icon'
              link_url = ebscolink['Url'] || 'detail'
              links.push({url: link_url, label: link_label, icon: link_icon, type: 'ebook-epub'})
            end
          end
        end

        items = @record.fetch('Items',{})
        if items.count > 0
          items.each do |item|
            if item['Group'] == 'URL'
              if item['Data'].include? 'linkTerm="'
                link_start = item['Data'].index('linkTerm="')+15
                link_url = item['Data'][link_start..-1]
                link_end = link_url.index('"')-1
                link_url = link_url[0..link_end]
                if item['Label']
                  link_label = item['Label']
                else
                  link_label_start = item['Data'].index('link>')+8
                  link_label = item['Data'][link_label_start..-1]
                  link_label = link_label.strip
                end
              else
                link_url = item['Data']
                link_label = item['Label']
              end
              link_icon = 'Catalog Link Icon'
              links.push({url: link_url, label: link_label, icon: link_icon, type: 'cataloglink'})
            end
          end
        end

        if ebscolinks.count > 0
          ebscolinks.each do |ebscolink|
            if ebscolink['Type'] == 'other'
              link_label = 'Linked Full Text'
              link_icon = 'Linked Full Text Icon'
              link_url = ebscolink['Url'] || 'detail'
              links.push({url: link_url, label: link_label, icon: link_icon, type: 'smartlinks+'})
            end
          end
        end

        ft_customlinks = @record.fetch('FullText',{}).fetch('CustomLinks',{})
        if ft_customlinks.count > 0
          ft_customlinks.each do |ft_customlink|
            link_url = ft_customlink['Url']
            link_label = ft_customlink['Text']
            link_icon = ft_customlink['Icon']
            links.push({url: link_url, label: link_label, icon: link_icon, type: 'customlink-fulltext'})
          end
        end

        links
      end

      # All available non-fulltext links.
      def non_fulltext_links
        links = []
        other_customlinks = @record.fetch('CustomLinks',{})
        if other_customlinks.count > 0
          other_customlinks.each do |other_customlink|
            link_url = other_customlink['Url']
            link_label = other_customlink['Text']
            link_icon = other_customlink['Icon']
            links.push({url: link_url, label: link_label, icon: link_icon, type: 'customlink-other'})
          end
        end

        links
      end

      #:nodoc: all
      # No need to document methods below

      # ====================================================================================
      # HEADER: DbId, DbLabel, An, PubType, PubTypeId, AccessLevel
      # ====================================================================================

      def header_an
        @record['Header']['An'].to_s
      end

      def header_db_id
        @record['Header']['DbId'].to_s
      end

      # only available from search not retrieve
      def header_score
        @record['Header']['RelevancyScore']
      end

      def header_publication_type
        @record['Header']['PubType']
      end

      def header_publication_type_id
        @record['Header']['PubTypeId']
      end

      def header_db_label
        @record['Header']['DbLabel']
      end

      # not sure the rules for when this appears or not - RecordInfo.AccessInfo?
      def header_access_level
        @record['Header']['AccessLevel']
      end

      # ====================================================================================
      # ITEMS
      # ====================================================================================

      # look up by 'Name' and return 'Data'
      def get_item_data_by_name(name)
        if @items.empty?
          nil
        else
          _item_property = @items.find{|item| item['Name'] == name}
          if _item_property.nil?
            nil
          else
            _item_property['Data']
          end
        end
      end

      # look up by 'Label' and return 'Data'
      def get_item_data_by_label(label)
        if @items.empty?
          nil
        else
          _item_property = @items.find{|item| item['Label'] == label}
          if _item_property.nil?
            nil
          else
            _item_property['Data']
          end
        end
      end

      def item_related_isbns
        isbns = get_item_data_by_label('Related ISBNs')
        if isbns
          isbns.split(' ').map!{|item| item.gsub(/\.$/, '')}
        else
          nil
        end
      end

      # ====================================================================================
      # BIB ENTITY
      # ====================================================================================

      def bib_title
        if @bib_entity && @bib_entity.fetch('Titles', {}).any?
          @bib_entity.fetch('Titles', {}).find{|item| item['Type'] == 'main'}['TitleFull']
        else
          nil
        end
      end

      def bib_authors
        if @bib_relationships
          @bib_relationships.deep_find('NameFull').join('; ')
        else
          nil
        end
      end

      def bib_authors_list
        if @bib_relationships
          @bib_relationships.deep_find('NameFull')
        else
          nil
        end
      end

      def bib_subjects
        if @bib_entity
          @bib_entity.deep_find('SubjectFull')
        else
          nil
        end
      end

      def bib_languages
        if @bib_entity && @bib_entity.fetch('Languages', {}).any?
          @bib_entity.fetch('Languages', {}).map{|lang| lang['Text']}
        else
          nil
        end
      end

      # def bib_pages
      #   @bib_entity.fetch('PhysicalDescription', {})['Pagination']
      # end

      def bib_page_count
        if @bib_entity
          @bib_entity.deep_find('PageCount').first
        else
          nil
        end
      end

      def bib_page_start
        if @bib_entity
          @bib_entity.deep_find('StartPage').first
        else
          nil
        end
      end

      def bib_doi
        if @bib_entity && @bib_entity.fetch('Identifiers',{}).any?
          @bib_entity.fetch('Identifiers',{}).find{|item| item['Type'] == 'doi'}['Value']
        else
          nil
        end
      end

      # ====================================================================================
      # BIB - IS PART OF (journal, book)
      # ====================================================================================

      def bib_source_title
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Titles',{}).any?
          item_title_full = @bib_part.fetch('BibEntity',{}).fetch('Titles',{}).find{|item| item['Type'] == 'main'}
          if item_title_full
            item_title_full['TitleFull']
          else
            nil
          end
        else
          nil
        end
      end

      def bib_issn_print
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).any?
          item_issn_p = @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).find{|item| item['Type'] == 'issn-print'}
          if item_issn_p
            item_issn_p['Value']
          else
            nil
          end
        else
          nil
        end
      end

      def bib_issn_electronic
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).any?
          item_issn_e = @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).find{|item| item['Type'] == 'issn-electronic'}
          if item_issn_e
            item_issn_e['Value']
          else
            nil
          end
        else
          nil
        end
      end

      def bib_issns
        issns = []
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).any?
          @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).each do |id|
            if id['Type'].include?('issn') && !id['Type'].include?('locals')
              issns.push(id['Value'])
            end
          end
        end
        issns
      end

      def bib_isbn_print
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).any?
          item_isbn_p = @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).find{|item| item['Type'] == 'isbn-print'}
          if item_isbn_p
            item_isbn_p['Value']
          else
            nil
          end
        else
          nil
        end
      end

      def bib_isbn_electronic
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).any?
          item_isbn_e = @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).find{|item| item['Type'] == 'isbn-electronic'}
          if item_isbn_e
            item_isbn_e['Value']
          else
            nil
          end
        else
          nil
        end
      end

      # todo: make this generic and take an optional parameter for type
      def bib_isbns
        isbns = []
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).any?
          @bib_part.fetch('BibEntity',{}).fetch('Identifiers',{}).each do |id|
            if id['Type'].include?('isbn') && !id['Type'].include?('locals')
              isbns.push(id['Value'])
            end
          end
        end
        isbns
      end

      def bib_publication_date
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Dates',{}).any?
          _date = @bib_part.fetch('BibEntity',{}).fetch('Dates',{}).find{|item| item['Type'] == 'published'}
          if _date
            if _date.has_key?('Y') && _date.has_key?('M') && _date.has_key?('D')
              _date['Y'] + '-' + _date['M'] + '-' + _date['D']
            else
              nil
            end
          else
            nil
          end
        else
          nil
        end
      end

      def bib_publication_year
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Dates',{}).any?
          _date = @bib_part.fetch('BibEntity',{}).fetch('Dates',{}).find{|item| item['Type'] == 'published'}
          if _date
            _date.has_key?('Y') ? _date['Y'] : nil
          else
            nil
          end
        else
          nil
        end
      end

      def bib_publication_month
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Dates',{}).any?
          _date = @bib_part.fetch('BibEntity',{}).fetch('Dates',{}).find{|item| item['Type'] == 'published'}
          if _date
            _date.has_key?('M') ? _date['M'] : nil
          else
            nil
          end
        else
          nil
        end
      end

      def bib_volume
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Numbering',{}).any?
          item_volume = @bib_part.fetch('BibEntity',{}).fetch('Numbering',{}).find{|item| item['Type'] == 'volume'}
          if item_volume
            item_volume['Value']
          else
            nil
          end
        else
          nil
        end
      end

      def bib_issue
        if @bib_part && @bib_part.fetch('BibEntity',{}).fetch('Numbering',{}).any?
          item_issue = @bib_part.fetch('BibEntity',{}).fetch('Numbering',{}).find{|item| item['Type'] == 'issue'}
          if item_issue
            item_issue['Value']
          else
            nil
          end
        else
          nil
        end
      end


      # Experimental bibtex support.
      def retrieve_bibtex

        @bibtex.key = accession_number
        @bibtex.title = title.gsub('<highlight>', '').gsub('</highlight>', '')
        if bib_authors_list.length > 0
          @bibtex.author = bib_authors_list.join(' and ').chomp
        end
        @bibtex.year = publication_year.to_i

        # bibtex type
        _type = publication_type
        case _type
          when 'Academic Journal', 'Reference'
            @bibtex.type = :article
            @bibtex.journal = source_title
            unless issue.nil?
              @bibtex.issue = issue
            end
            unless volume.nil?
              @bibtex.number = volume
            end
            if page_start && page_count
              @bibtex.pages = page_start + '-' + (page_start.to_i + page_count.to_i-1).to_s
            end
            if bib_publication_month
              @bibtex.month = bib_publication_month.to_i
            end
            if doi
              @bibtex.doi = doi
              @bibtex.url = 'https://doi.org/' + doi
            end
          when 'Conference'
            @bibtex.type = :conference
            @bibtex.booktitle = source_title
            if issue
              @bibtex.issue = issue
            end
            if volume
              @bibtex.number = volume
            end
            if page_start && page_count
              @bibtex.pages = page_start + '-' + (page_start.to_i + page_count.to_i-1).to_s
            end
            if bib_publication_month
              @bibtex.month = bib_publication_month.to_i
            end
            if publisher_info
              @bibtex.publisher = publisher_info
            end
            if series
              @bibtex.series = series
            end
          when 'Book', 'eBook'
            @bibtex.type = :book
            if publisher_info
              @bibtex.publisher = publisher_info
            end
            if series
              @bibtex.series = series
            end
            if bib_publication_month
              @bibtex.month = bib_publication_month.to_i
            end
            if isbns
              @bibtex.isbn = isbns.first
            end
          else
            @bibtex.type = :other
        end
        @bibtex
      end

      ##
      # wrap bibtex entry in a bibliography so that it can be transformed into citations using citeproc
      def bibtex_bibliography
        bib = BibTeX::Bibliography.new
        bib << @bibtex
        bib
      end

      # this is used to generate solr fields
      def to_hash(type = 'compact')
        hash = {}

        # information typically required by all views
        if database_id && accession_number
          safe_an = accession_number.gsub(/\./,'_')
          hash['id'] = database_id + '__' + safe_an
        end
        unless title.nil?
          hash['title_display'] = title.gsub('<highlight>', '').gsub('</highlight>', '')
        end
        if source_title
          hash['academic_journal'] = source_title
        end
        if publication_year
          hash['pub_date'] = publication_year
        end
        if authors
          hash['author_display'] = authors.to_s
        end
        if publication_type
          hash['format'] = publication_type.to_s
        end
        if languages
          if languages.kind_of?(Array)
            hash['language_facet'] = languages.join(', ')
          else
            hash['language_facet'] = languages.to_s
          end
        end
        if publisher_info
          hash['pub_info'] = publisher_info
        end
        if abstract
          hash['abstract'] = abstract
        end
        if cover_thumb_url
          hash['cover_thumb_url'] = cover_thumb_url
        end
        if cover_medium_url
          hash['cover_medium_url'] = cover_medium_url
        end
        # generate bibtex entry if it hasn't been done already
        if @bibtex.key == 'unknown-a'
          @bibtex = retrieve_bibtex
        end
        unless @bibtex.has_type?(:other)
          hash['citation_apa'] = citation('apa').first.to_s
          hash['citation_mla'] = citation('modern-language-association').first.to_s
          hash['citation_chicago'] = citation('chicago-author-date').first.to_s
        end

        # extra information typically required by detailed item views
        if type == 'verbose'
          if all_links
            hash['links'] = all_links
          end
          if doi
            hash['doi'] = doi
          end
          if html_fulltext
            hash['html_fulltext'] = html_fulltext
          end
        end

        hash
      end

      def to_solr
        # solr response
        item_hash = to_hash 'verbose'
        solr_response =
        {
            'responseHeader' => {
                'status' => 0
            },
            'response' => {
                'numFound' => 1,
                'start' => 0,
                'docs' => [item_hash]
            }
        }
        # puts 'SOLR RESPONSE: ' + solr_response.inspect
        solr_response
      end

      def citation(style = 'apa')
        # generate bibtex entry if it hasn't been done already
        if @bibtex.key == 'unknown-a'
          @bibtex = retrieve_bibtex
        end
        # TODO: catch CSL::ParseError when style can't be found
        CSL::Style.root = File.join(__dir__, 'csl/styles')
        cp = CiteProc::Processor.new style: style, format: 'text'
        bib_entry = @bibtex
        bib_entry_id = bib_entry.to_citeproc['id']
        cp.import bibtex_bibliography.to_citeproc
        cp.render :bibliography, id: bib_entry_id
      end

    end # Class Record
  end # Module EDS
end # Module EBSCO


# monkey patches
class Hash
  def deep_find(key, object=self, found=[])
    if object.respond_to?(:key?) && object.key?(key)
      found << object[key]
    end
    if object.is_a? Enumerable
      found << object.collect { |*a| deep_find(key, a.last) }
    end
    found.flatten.compact
  end
end