#! /usr/bin/perl -T

# Create or edit existing database record from HTML form when for is POSTed;

my $dir;
BEGIN {
    use Cwd qw(abs_path);
    $dir = abs_path(__FILE__);
    $dir =~ /(.*)(\/.*\/.*\/.*\..*)$/;
    $dir = $1;
    unshift(@INC, $dir . "/lib");
}

use strict;
use warnings;
use CGI qw(:standard -nosticky -utf8 start_div end_div);
use DBI;
use Encode;
use File::Basename qw(basename dirname);
use HTML::Entities;
use HTML::FromText;
use Hash::Merge qw(merge);

use CGIParameters qw(read_cgi_parameters);
use Database qw( prepare_record_descriptions_for_save_as_new );
use Database::Order;
use Database::Filter;
use HTMLGenerator qw(
    data2html_page
    data2html_edit_form
    error_page
    start_html5
    transfer_query_string
    start_root
    end_root
    header_record_pl
    start_doc_wrapper
    end_doc_wrapper
    start_main_content
    end_main_content
    footer_db_pl
    footer_record_pl
    start_card_fluid
    end_card
    three_column_row_html
    two_column_row_html
    button_classes_string
    warnings2html
);
use RestfulDB::CGI qw( save_cgi_parameters );
use RestfulDB::Defaults qw( get_css_styles get_default_cgi_parameters );
use RestfulDB::DBSettings qw( get_database_settings );
use RestfulDB::Exception;
use RestfulDB::JSON qw( data2json error2json json2data );
use RestfulDB::JSONAPI qw( data2resource error2jsonapi jsonapi2data );
use RestfulDB::Spreadsheet qw( spreadsheet2data );

# STDIN must NOT be set to binmode "UTF-8", since CGI with the '-utf8'
# flag handles this.
binmode( STDOUT, "utf8" );
binmode( STDERR, "utf8" );

my $page_level = 2;

$ENV{PATH} = ""; # Untaint and clean PATH

# Path to database directory for SQlite3 databases:
my $db_dir = $dir . "/db";

Hash::Merge::set_behavior( 'RIGHT_PRECEDENT' );

my @warnings;
local $SIG{__WARN__} = sub { print STDERR $_[0]; push @warnings, $_[0] };

my $cgi = new CGI;
my $format = 'html';

# Detecting JSON/JSONAPI requests
my( $accept ) = Accept();
if(      (content_type() && content_type() eq $RestfulDB::JSONAPI::MEDIA_TYPE) ||
         ($accept && $accept eq $RestfulDB::JSONAPI::MEDIA_TYPE) ) {
    $format = 'jsonapi';
} elsif( (content_type() && content_type() eq $RestfulDB::JSON::MEDIA_TYPE) ||
         ($accept && $accept eq $RestfulDB::JSON::MEDIA_TYPE) ) {
    $format = 'json';
}

eval {

    my $filename_re = '[^\x{00}-\x{08}\x{0A}-\x{1F}\x{7F}]+';

    my %local_CGI_parameters = (
        Edit   => { re => 'Edit' },
        Update => { re => 'Update' },
        Save   => { re => 'Save' },
        'Save as new' => { re => 'Save as new' },
        Delete => { re => 'Delete' },
        depth => { re => '-1|[0-9]+', default => -1 },
        format => { re => 'html|csv|xml|cif|json(?:api)?',
                    default => 'html' },
        include => { re => '.*' },

        csvfile  => { re => $filename_re },
        odsfile  => { re => $filename_re },
        xlsfile  => { re => $filename_re },
        xlsxfile => { re => $filename_re },

        spreadsheet => { re => $filename_re },

        jsonfile => { re => $filename_re },

        filter  => { re => '.*' },
    );

    my( $base_uri, $query_string ) = split /\?/, $ENV{REQUEST_URI};

    my( $params, $changed );
    eval {
        ( $params, $changed ) =
            read_cgi_parameters( $cgi,
                                { %RestfulDB::Defaults::CGI_parameters,
                                  %local_CGI_parameters },
                                { passthrough_re => qr/^(column|action):/,
                                  query_string   => $query_string } );
    };
    InputException->throw( $@ ) if $@;

    my %params = %$params;

    if( $params{debug} && $params{debug} eq 'save' ) {
        save_cgi_parameters( $db_dir );
    }

    $format = $params{format} if $changed->{format};

    my %db_settings = get_database_settings( \%params, \%ENV,
                                             { db_dir => $db_dir,
                                               level => 2 });

    my $db_user = $db_settings{db_user};
    my $db_name = $db_settings{db_name};
    my $db_path = $db_settings{db_path};
    my $db_table = $db_settings{db_table};
    my $db_engine = $db_settings{db_engine};
    my $record_id = $db_settings{record_id};

    my $remote_user = $db_settings{remote_user_for_print};

    if( uc($ENV{REQUEST_METHOD}) ne 'GET' && !defined $remote_user ) {
        UnauthorizedException->throw(
            'Editing a database requires authentication' );
    }

    my $db_settings = {
        content_db => { DB => $db_path,
                        engine => $db_engine,
                        user => $db_user },
    };

    # Parameter 'include' is reserved for JSONAPI, and MUST trigger
    # 400 Bad Request error if requested but not implemented:
    if( $params->{include} ) {
        InputException->throw( "'include' parameter is not supported yet" );
    }

    my $db = Database->new( $db_settings );
    $db->connect( { RaiseError => 1, AutoCommit => 1 } );

    if( !$changed->{id_column} ) {
        $params{id_column} = $db->get_unique_key_column( $db_table, $record_id );
    }

    InputException->throw( 'Record ID is not supplied' ) if !defined $record_id;

    my $real_record_id = $db->get_id_by_extkey( $db_table,
                                                $record_id,
                                                { key_column => $params{id_column} } );
    my $real_id_column = $db->get_unique_key_column( $db_table, $real_record_id );

    my $web_base = dirname dirname $base_uri;
    my $list_uri = dirname( $ENV{REQUEST_URI} );
    if( $ENV{REQUEST_URI} =~ /(\?.*)$/ ) {
        # Append the Query string:
        $list_uri .= $1;
    }

    my $html;
    my @js_imports = qw( form_validation.js
                         jquery.js
                         esc_key_press.js );
    my $js_onload_action;

    my $HTTP_status = '200 OK';
    my $location;

    if( uc($ENV{REQUEST_METHOD}) eq 'POST' && $params{'Save as new'} ) {
        # Create new the database records:
        my $data = $db->get_record_descriptions( $db_table,
                                                 { template => 1 } );
        my $record_data =
            $db->form_parameters_to_descriptions( $data, \%params,
                                                  { default_action => 'insert',
                                                    cgi => $cgi } );
        prepare_record_descriptions_for_save_as_new( $record_data );

        my( $uuids ) = $db->modify_record_descriptions( $record_data );
        my $redirect_uri = dirname( $ENV{REQUEST_URI} );
        # FIXME: hardcoded uuid column name
        if( @$uuids && defined $uuids->[0]{uuid} &&
            $db->get_uuid_column( $db_table ) ) {
            $redirect_uri .= "/$uuids->[0]{uuid}";
        }
        print $cgi->redirect(
                transfer_query_string( $redirect_uri,
                                       $ENV{REQUEST_URI} ) );
        exit( 0 );
    } elsif( uc($ENV{REQUEST_METHOD}) eq 'POST' && $params{'Edit'} ) {
        @js_imports = qw( form_control.js
                          form_validation.js
                          jquery.js
                          esc_key_press.js
                          scroll_into_view.js
                          select2.js
                          dropdown.js );
        $js_onload_action = 'unhide_form_buttons';

        my $data =
            $db->get_record_descriptions( $db_table,
                                          {
                                              id_column => $real_id_column,
                                              record_id => $real_record_id,
                                              show_fk_values => 1,
                                              show_enumeration_values => 1,
                                              web_base => $web_base,
                                          } );

        my $fk_columns  = $db->get_foreign_keys($db_table);
        my $fk_table_data = $db->get_fks_cell_text( $fk_columns, 1 );
        my $ambiguous_fks = {};

        foreach my $fk( sort keys %{ $fk_table_data->{fk_values} } ){
            my $fk_table_values = $fk_table_data->{fk_values}{$fk};
            $ambiguous_fks->{ $fk } =
             HTMLGenerator::get_ambiguous_fk_values($fk_table_values, $fk, $fk_columns);
        }

        my $buttons =
            submit( { -name  => 'Update',
                      -value => 'Update',
                      -class => button_classes_string(),
                      -onclick => 'clearView(); return validate_and_submit( this.form )' } ) . "\n" .
            submit( { -name  => 'Save as new',
                      -value => 'Save as new',
                      -class => button_classes_string(),
                      -onclick => 'clearView(); return validate_and_submit( this.form )' } );
        $html .=  "\n" .
            start_form( -action => $ENV{REQUEST_URI},
                        -id => 'record-delete-form',
                        -method => 'post' )
                        . "\n" .
            submit( -name => 'Delete',
                    -value => 'Delete',
                    -class => button_classes_string('secondary') )
                    . "\n" .
            end_form . "\n";
        $html .= data2html_edit_form(  $db, $data, $db_table,
                                  {
                                      request_uri => $ENV{REQUEST_URI},
                                      level => 2,
                                      vertical => 1,
                                      add_button_as_link => 1,
                                      table_properties =>
                                      {
                                          -class => 'record-hoverable dbdata-input-form',
                                      },
                                      form_head => $buttons,
                                      form_tail => br . $buttons,
                                      ambiguous_fks => $ambiguous_fks
                                  } );
    } elsif( uc($ENV{REQUEST_METHOD}) eq 'DELETE' || $params{Delete} ) {
        # Delete the database record:
        $db->delete_record( $real_record_id, $db_table,
                            { id_column => $real_id_column } );
        CGI::Delete_all();
        my $message = sprintf 'Record \'%s\' was deleted.',
                              defined $real_record_id
                                ? $real_record_id : $record_id;
        if( $format =~ /^json(api)?$/ ) {
            my %MEDIA_TYPE = (
                json    => $RestfulDB::JSON::MEDIA_TYPE,
                jsonapi => $RestfulDB::JSONAPI::MEDIA_TYPE,
            );
            my $meta = $format eq 'json' ? 'metadata' : 'meta';
            print $cgi->header( -type => $MEDIA_TYPE{$format},
                                -charset => 'UTF-8' ),
                  JSON->new()->canonical->encode(
                    {
                      $meta => {
                        detail => $message,
                        @warnings ? (warnings => \@warnings) : ()
                      }
                    }
                  ),
                  "\n";
        } else {
            $html = p( $message ) . "\n";
        }
        undef $real_record_id;
    } elsif( uc($ENV{REQUEST_METHOD}) eq 'PUT' ||
             uc($ENV{REQUEST_METHOD}) eq 'POST' ||
             uc($ENV{REQUEST_METHOD}) eq 'PATCH' ) {

        my $default_action = 'insert';
        if( exists $params{"Update"} || uc($ENV{REQUEST_METHOD}) eq 'PATCH' ) {
            $default_action = 'update';
        } elsif( content_type() && content_type() eq $RestfulDB::JSONAPI::MEDIA_TYPE &&
                 uc($ENV{REQUEST_METHOD}) eq 'POST' ) {
            $default_action = 'insert';
        }

        my $get_record_descriptions_options = {
            no_views => 1,
            show_fk_values => 1,
            show_enumeration_values => 1,
        };
        if( defined $real_record_id && !$params{"Save"} ) {
            $get_record_descriptions_options->{id_column} = $real_id_column;
            $get_record_descriptions_options->{record_id} = $real_record_id;
            $get_record_descriptions_options->{no_empty}  = 1;
        } else {
            $get_record_descriptions_options->{template} = 1;
        }

        my $record_data;
        my %options;
        if( exists $params{"Save"} || exists $params{"Update"} ) {
            if( uc($ENV{REQUEST_METHOD}) ne 'POST' ) {
                InputException->throw(
                    'Save/Update requests must be sent via HTTP POST method' );
            }
            my $data = $db->get_record_descriptions( $db_table,
                                                     $get_record_descriptions_options );
            $record_data =
                $db->form_parameters_to_descriptions( $data, \%params,
                                                      { default_action => $default_action,
                                                        cgi => $cgi } );
        } elsif( defined $params{csvfile}     || defined $params{xlsfile} ||
                 defined $params{xlsxfile}    || defined $params{odsfile} ||
                 defined $params{spreadsheet} ||
                 (content_type() && exists $RestfulDB::Spreadsheet::content_types{content_type()}) ) {
            my $data = $db->get_record_descriptions( $db_table,
                                                     $get_record_descriptions_options );
            $record_data = spreadsheet2data( $db, $data, \%params, $cgi, $db_table );
            $options{duplicates} = 'update';
        } elsif( exists $params{'jsonfile'} ||
                (content_type() && content_type() eq $RestfulDB::JSON::MEDIA_TYPE) ) {

            my $json;
            if(  content_type() && content_type() eq $RestfulDB::JSON::MEDIA_TYPE ) {
                $json = decode( 'UTF-8', $cgi->param( $ENV{REQUEST_METHOD} . 'DATA' ) );
            } else {
                my $file = $cgi->upload( 'jsonfile' );
                InputException->throw( "No upload file handle?" ) unless $file;
                $json = decode( 'UTF-8', join '', <$file> );
            }

            $record_data = json2data( $json, { db => $db,
                                               default_action => $default_action } );
            $record_data = $record_data->[0];

            my $record_description;
            if( defined $real_record_id ) {
                $record_description =
                    $db->get_record_description( $record_data->{metadata}{table_name},
                                                 { record_id => $real_record_id } );
            } else {
                $record_description =
                    $db->get_record_description( $record_data->{metadata}{table_name},
                                                 { template => 1 } );
            }
            $record_data = [ merge( $record_description, $record_data ) ];

            if( uc($ENV{REQUEST_METHOD}) ne 'PUT' ) {
                # PUT to an existing record is reported as conflict.
                $options{duplicates} = 'update';
            }
        } elsif( content_type() && content_type() eq $RestfulDB::JSONAPI::MEDIA_TYPE ) {
            if( uc($ENV{REQUEST_METHOD}) eq 'PUT' ) {
                InputException->throw( 'HTTP PUT request method is not supported by ' .
                                       'JSON API' );
            }
            my $json = decode( 'UTF-8', $cgi->param( $ENV{REQUEST_METHOD} . 'DATA' ) );
            $record_data = jsonapi2data( $db,
                                         $json,
                                         { request_id => $real_record_id,
                                           default_action => $default_action } );
        } elsif( $format ) {
            InputException->throw( "Unknown input format '$format'." );
        } else {
            InputException->throw( 'Unknown input method. Please contact ' .
                                   'the site administrator to resolve this issue.' );
        }

        if( @$record_data != 1 ) {
            InputException->throw(
                scalar( @$record_data ) .
                " records submitted, only one is allowed." );
        }

        if( !defined $record_data->[0]{columns}{$params{id_column}}{value} ) {
            $record_data->[0]{columns}{$params{id_column}}{value} = $record_id;
        } elsif( $record_data->[0]{columns}{$params{id_column}}{value} ne
                 $record_id ) {
            # Public keys can only be changed by using alternative public
            # keys. Thus a value of 'id' cannot be changed by referring via
            # '<table>/<id>'.
            my $request_body_id =
                $record_data->[0]{columns}{$params{id_column}}{value};
            InputException->throw(
                "Keys of the request URI ('$record_id') and body " .
                "('$request_body_id') do not match" );
        }

        my( $entries, $dbrevision_id ) =
            $db->modify_record_descriptions( $record_data, \%options );

        my $dbrev_columns = [ $db->get_column_of_kind( $db_table, 'dbrev' ) ];
        if( defined $real_record_id ) {
            # In principle, the real record ID could have been changed,
            # thus it is necessary to update it.
            $real_record_id =
                $db->get_id_by_extkey( $db_table,
                                       $record_id,
                                       { key_column => $params{id_column} } );
        } elsif( defined $dbrevision_id && @$dbrev_columns ) {
            $real_record_id =
                $db->get_id_by_extkey( $db_table,
                                       $dbrevision_id,
                                       { key_column => $dbrev_columns->[0] } );
        } else {
            my $uuid_column = $db->get_uuid_column( $db_table );
            my @uids;
            for my $entry (@$entries) {
                my( $uid_column ) = grep { exists $entry->{$_} &&
                                           defined $entry->{$_} }
                                         ($uuid_column ? $uuid_column : (),
                                          sort keys %$entry);
                next if !$uid_column;
                $real_record_id =
                    $db->get_id_by_extkey( $db_table,
                                           $entry->{$uid_column},
                                           { key_column => $uid_column } );
                last;
            }
            # FIXME: if no ID is found, use SELECT MAX(id) FROM $db_table
        }
        $real_id_column = $db->get_unique_key_column( $db_table, $real_record_id );
        if( $default_action eq 'insert' ) {
            $HTTP_status = '201 Created';
            $location = $ENV{REQUEST_URI};
        }
    } elsif( uc($ENV{REQUEST_METHOD}) ne 'GET' ) {
        InputException->throw(
            "Unknown HTTP request method '$ENV{REQUEST_METHOD}'. " .
            'Please contact your Web site administrator to fix this problem.' );
    }

    my $order = Database::Order->new_from_string( $params->{order},
                                                  $db_table );
    my $comparison = OPTIMADE::Filter::Comparison->new( '=' );
    if( defined $real_record_id ) {
        $comparison->left(
            OPTIMADE::Filter::Property->new( $db_table, $real_id_column ) );
        $comparison->right( $real_record_id );
    } else {
        # This is a hack to match no records
        $comparison->left( 0 );
        $comparison->right( 1 );
    }
    my $filter = Database::Filter->new_from_tree( $comparison );
    my $two_column_row_html_options = {};

    if(  uc($ENV{REQUEST_METHOD}) ne 'DELETE' &&
        (uc($ENV{REQUEST_METHOD}) ne 'POST' || (!$params{Edit} && !$params{Delete})) ) {
        # Show the database record:
        my $data =
            $db->get_record_descriptions( $db_table,
                                          {
                                              id_column => $real_id_column,
                                              depth => $params{depth},
                                              filter => $filter,
                                              order => $order,
                                              no_empty => 1,
                                              web_base => $web_base,
                                          } );

        if( $format =~ /^json(api)?$/ ) {
            my $filename = sprintf '%s-%s.json',
                                   $db_table,
                                   defined $real_record_id ? $real_record_id : $record_id;
            if( $format eq 'json' ) {
                print $cgi->header( -type => $RestfulDB::JSON::MEDIA_TYPE,
                                    -expires => 'now',
                                    -charset => 'UTF-8',
                                    -status  => $HTTP_status,
                                    ($location ? ( -location => $location ) : ()),
                                    -attachment => $filename ),
                      data2json( $db, $data, $db_table,
                                 { request_uri => $ENV{REQUEST_URI},
                                   web_base => dirname( dirname( url() ) ),
                                   warnings => \@warnings } );
            } else {
                print $cgi->header( -type => $RestfulDB::JSONAPI::MEDIA_TYPE,
                                    -expires => 'now',
                                    -charset => 'UTF-8',
                                    -status  => $HTTP_status,
                                    ($location ? ( -location => $location ) : ()),
                                    -attachment => $filename ),
                      data2resource( $db, $data, $db_table,
                                     { request_uri => $ENV{REQUEST_URI},
                                       web_base => dirname( dirname( url() ) ),
                                       warnings => \@warnings } );
            }
            print "\n";
        } elsif( $format eq 'html' ) {
            my $filter = Database::Filter->new( { filter => $params->{filter} } );
            
            my $fk_columns  = $db->get_foreign_keys($db_table);
            my $fk_table_data = $db->get_fks_cell_text( $fk_columns, 1 );
            my $ambiguous_fks = {};

            foreach my $fk( sort keys %{ $fk_table_data->{fk_values} } ){
                my $fk_table_values = $fk_table_data->{fk_values}{$fk};
                $ambiguous_fks->{ $fk } =
                HTMLGenerator::get_ambiguous_fk_values($fk_table_values, $fk, $fk_columns);
            }

            # Collect record data preprint html.
            # Variables set to split to page elements.
            # FIXME:
            # 'Back to selection' and buttons for now are generated in
            # HTMLGenerator.
            my( $table_html,
                $head_html_w_navigation_buttons,
                $record_modification_buttons ) =
                data2html_page( $db, $data, $db_table,
                                {
                                    id_column => $params{id_column},
                                    record_id => $real_record_id,
                                    order => $order,
                                    filter => $filter,
                                    level => 2,
                                    vertical => 1,
                                    table_properties =>
                                    {
                                        -class => 'record-hoverable'
                                    },
                                    request_uri => $ENV{REQUEST_URI},
                                    rows => $params{rows},
                                    offset => $params{offset},
                                    ambiguous_fks => $ambiguous_fks
                                } );

            $two_column_row_html_options = {
                row_id => 'record-navigation-buttons',
                column_minor => $head_html_w_navigation_buttons,
                scale_lg => 6,
                scale_md => 12
            };

            $html = $record_modification_buttons . $table_html;
        } else {
            InputException->throw( "Unsupported format '$format'" );
        }
    }

    if( $html ) {
        my @script = map { { -type => 'text/javascript',
                             -src  => "../../js/$_" } } @js_imports;
        if( $js_onload_action ) {
            push @script, "window.onload = $js_onload_action";
        }

        # Acknowledge the user name:
        if( defined $remote_user ) {
            # 'doc' class for paragraphs ensures that their content
            # can be center-aligned.
            my $column_right_html = p( {-class => "doc"}, "User: $remote_user" );
            if( $two_column_row_html_options->{column_major} ) {
                $two_column_row_html_options->{column_major} .=
                    " $column_right_html";
            } else {
                $two_column_row_html_options->{column_major} =
                    $column_right_html;
            }
        }

        # Composing the URI for the next page of the selection
        my $selection_uri;
        if( defined $real_record_id ) {
            my $nbr_rec_info 
                = $db->get_neighbour_ids( $db_table,
                                         { record_id => $real_record_id,
                                           order => $order,
                                           filter => $filter,
                                           selected_column => $params{id_column} });
            my $current_offset =
                int( ($nbr_rec_info->{curr_rec} - 1) / $params{rows} ) *
                $params{rows};

            my $request_uri = $ENV{REQUEST_URI};
            $selection_uri = dirname( $request_uri );
            if( $request_uri =~ /(\?.*)$/ ) {
                 $selection_uri .= $1;
            }

            my $base_uri = $selection_uri;
            $base_uri =~ s/\?.*$//;

            my $QS_append = {};
            $QS_append->{offset} = $current_offset if $current_offset;
            $selection_uri =
                transfer_query_string( $base_uri,
                                       $selection_uri,
                                       { exclude_re => 'offset',
                                         append => $QS_append } );
        }

        # FIXME: get '$selection_uri' from the HTMLGenerator
        # module as to not to cause more divergence from the original
        # code html text.
        # $selection_uri VS $list_uri : there is no '?offset=0' in
        # '$list_uri' outputs.
        # Need to consult if there might be more significant differences.
        # If not: keep $list_uri string and drop $selection_uri from the
        # data2html_page output altogether.
        my $selection_filter_string = '';
        if( defined $selection_uri ) {
            if( $selection_uri =~ /(\?.*)$/ ) {
                $selection_filter_string = $1;
            }
        } else {
            if( $list_uri =~ /(\?.*)$/ ) {
                $selection_filter_string = $1;
            }
        }

        # This ends the HTML page.
        $html = $cgi->header( -type => 'text/html',
                              -expires => 'now',
                              -charset => 'UTF-8',
                              -status  => $HTTP_status,
                              ($location ? (-location => $location ) : ()) ) .
            start_html5( $cgi, { -title => "$record_id record - $db_table table - $db_name db: " .
                                           $RestfulDB::Defaults::website_title,
                                 -head => Link({
                                                -rel  => 'icon',
                                                -type => 'image/x-icon',
                                                -href => '../../images/favicon.ico'
                                 }),
                                 -meta => {
                                   'viewport' => 'width=device-width, initial-scale=1',
                                 },
                                 -script => \@script,
                                 -style => get_css_styles( $db_name, $page_level ) } ) .

            # The beginning of the HTML page.
            start_root() .
            header_record_pl( $db_name,
                              $db_table,
                              $selection_filter_string,
                              $record_id ) .
            start_doc_wrapper() .
            start_main_content() .

            # Hidden 'top' paragraph on the data page for the 'To top' link.
            p( { -id => 'top' }, 'Top' ) . "\n" .

            # Warnings
            warnings2html( @warnings ) .

            # Start record navigation card.
            start_card_fluid( { class => 'record-navigation-panel' } ) .
            # Setting record_id to reside in major column displays record id info
            # in a single centered column.
            two_column_row_html( {
                column_major => "Record id: $record_id",
                row_class => 'page-title'
            } ) .

            two_column_row_html( $two_column_row_html_options ) .
            end_card() . # Close the first card.

            # Start the main data card.
            start_card_fluid() .
            start_div( { -class => 'section' } ) . "\n" .

            $html;

        $html .= end_div() . "\n"; # Close the first section.
        $html .= end_card(); # Close the second card.
        $html .= footer_record_pl( $db_name,
                                   $db_table,
                                   $selection_filter_string,
                                   $record_id);
        $html .= end_main_content();
        $html .= end_doc_wrapper();
        $html .= end_root();
        $html .= end_html . "\n";
        
        print $html;
    }

    $db->disconnect();
};

if( $@ ) {
    if(      $format eq 'json' ) {
        error2json( $cgi, $@ );
    } elsif( $format eq 'jsonapi' ) {
        error2jsonapi( $cgi, $@ );
    } else {
        error_page( $cgi, $@, $page_level );
    }
}
