/** @file resetapi.cpp
    web rest api
*/
/**
*    Copyright (C) 2008 10gen Inc.
*
*    This program is free software: you can redistribute it and/or  modify
*    it under the terms of the GNU Affero General Public License, version 3,
*    as published by the Free Software Foundation.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU Affero General Public License for more details.
*
*    You should have received a copy of the GNU Affero General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "pch.h"
#include "../util/net/miniwebserver.h"
#include "../util/mongoutils/html.h"
#include "../util/md5.hpp"
#include "instance.h"
#include "dbwebserver.h"
#include "dbhelpers.h"
#include "repl.h"
#include "replutil.h"
#include "clientcursor.h"
#include "background.h"

#include "restapi.h"

namespace mongo {

    extern const char *replInfo;
    bool getInitialSyncCompleted();

    using namespace bson;
    using namespace mongoutils::html;

    class RESTHandler : public DbWebHandler {
    public:
        RESTHandler() : DbWebHandler( "DUMMY REST" , 1000 , true ) {}

        virtual bool handles( const string& url ) const {
            return
                url[0] == '/' &&
                url.find_last_of( '/' ) > 0;
        }

        virtual void handle( const char *rq, string url, BSONObj params,
                             string& responseMsg, int& responseCode,
                             vector<string>& headers,  const SockAddr &from ) {

            string::size_type first = url.find( "/" , 1 );
            if ( first == string::npos ) {
                responseCode = 400;
                return;
            }

            string method = MiniWebServer::parseMethod( rq );
            string dbname = url.substr( 1 , first - 1 );
            string coll = url.substr( first + 1 );
            string action = "";

            string::size_type last = coll.find_last_of( "/" );
            if ( last == string::npos ) {
                action = coll;
                coll = "_defaultCollection";
            }
            else {
                action = coll.substr( last + 1 );
                coll = coll.substr( 0 , last );
            }

            for ( string::size_type i=0; i<coll.size(); i++ )
                if ( coll[i] == '/' )
                    coll[i] = '.';

            string fullns = MiniWebServer::urlDecode(dbname + "." + coll);

            headers.push_back( (string)"x-action: " + action );
            headers.push_back( (string)"x-ns: " + fullns );

            bool html = false;


            stringstream ss;

            if ( method == "GET" ) {
                responseCode = 200;
                html = handleRESTQuery( fullns , action , params , responseCode , ss  );
            }
            else if ( method == "POST" ) {
                responseCode = 201;
                handlePost( fullns , MiniWebServer::body( rq ) , params , responseCode , ss  );
            }
            else {
                responseCode = 400;
                headers.push_back( "X_err: bad request" );
                ss << "don't know how to handle a [" << method << "]";
                out() << "don't know how to handle a [" << method << "]" << endl;
            }

            if( html )
                headers.push_back("Content-Type: text/html;charset=utf-8");
            else
                headers.push_back("Content-Type: text/plain;charset=utf-8");

            responseMsg = ss.str();
        }

        bool handleRESTQuery( string ns , string action , BSONObj & params , int & responseCode , stringstream & out ) {
            Timer t;

            int html = _getOption( params["html"] , 0 );
            int skip = _getOption( params["skip"] , 0 );
            int num  = _getOption( params["limit"] , _getOption( params["count" ] , 1000 ) ); // count is old, limit is new

            int one = 0;
            if ( params["one"].type() == String && tolower( params["one"].valuestr()[0] ) == 't' ) {
                num = 1;
                one = 1;
            }

            BSONObjBuilder queryBuilder;

            BSONObjIterator i(params);
            while ( i.more() ) {
                BSONElement e = i.next();
                string name = e.fieldName();
                if ( ! name.find( "filter_" ) == 0 )
                    continue;

                string field = name.substr(7);
                const char * val = e.valuestr();

                char * temp;

                // TODO: this is how i guess if something is a number.  pretty lame right now
                double number = strtod( val , &temp );
                if ( temp != val )
                    queryBuilder.append( field , number );
                else
                    queryBuilder.append( field , val );
            }

            BSONObj query = queryBuilder.obj();
            auto_ptr<DBClientCursor> cursor = db.query( ns.c_str() , query, num , skip );
            uassert( 13085 , "query failed for dbwebserver" , cursor.get() );

            if ( one ) {
                if ( cursor->more() ) {
                    BSONObj obj = cursor->next();
                    out << obj.jsonString(Strict,html?1:0) << '\n';
                }
                else {
                    responseCode = 404;
                }
                return html != 0;
            }

            if( html )  {
                string title = string("query ") + ns;
                out << start(title)
                    << p(title)
                    << "<pre>";
            }
            else {
                out << "{\n";
                out << "  \"offset\" : " << skip << ",\n";
                out << "  \"rows\": [\n";
            }

            int howMany = 0;
            while ( cursor->more() ) {
                if ( howMany++ && html == 0 )
                    out << " ,\n";
                BSONObj obj = cursor->next();
                if( html ) {
                    if( out.tellp() > 4 * 1024 * 1024 ) {
                        out << "Stopping output: more than 4MB returned and in html mode\n";
                        break;
                    }
                    out << obj.jsonString(Strict, html?1:0) << "\n\n";
                }
                else {
                    if( out.tellp() > 50 * 1024 * 1024 ) // 50MB limit - we are using ram
                        break;
                    out << "    " << obj.jsonString();
                }
            }

            if( html ) {
                out << "</pre>\n";
                if( howMany == 0 ) out << p("Collection is empty");
                out << _end();
            }
            else {
                out << "\n  ],\n\n";
                out << "  \"total_rows\" : " << howMany << " ,\n";
                out << "  \"query\" : " << query.jsonString() << " ,\n";
                out << "  \"millis\" : " << t.millis() << '\n';
                out << "}\n";
            }

            return html != 0;
        }

        // TODO Generate id and revision per couch POST spec
        void handlePost( string ns, const char *body, BSONObj& params, int & responseCode, stringstream & out ) {
            try {
                BSONObj obj = fromjson( body );
                db.insert( ns.c_str(), obj );
            }
            catch ( ... ) {
                responseCode = 400; // Bad Request.  Seems reasonable for now.
                out << "{ \"ok\" : false }";
                return;
            }

            responseCode = 201;
            out << "{ \"ok\" : true }";
        }

        int _getOption( BSONElement e , int def ) {
            if ( e.isNumber() )
                return e.numberInt();
            if ( e.type() == String )
                return atoi( e.valuestr() );
            return def;
        }

        DBDirectClient db;

    } restHandler;

    bool RestAdminAccess::haveAdminUsers() const {
        readlocktryassert rl("admin.system.users", 10000);
        Client::Context cx( "admin.system.users", dbpath, NULL, false );
        return ! Helpers::isEmpty("admin.system.users", false);
    }

    BSONObj RestAdminAccess::getAdminUser( const string& username ) const {
        Client::GodScope gs;
        readlocktryassert rl("admin.system.users", 10000);
        Client::Context cx( "admin.system.users" );
        BSONObj user;
        if ( Helpers::findOne( "admin.system.users" , BSON( "user" << username ) , user ) )
            return user.copy();
        return BSONObj();
    }

    class LowLevelMongodStatus : public WebStatusPlugin {
    public:
        LowLevelMongodStatus() : WebStatusPlugin( "low level" , 5 , "requires read lock" ) {}

        virtual void init() {}

        void _gotLock( int millis , stringstream& ss ) {
            ss << "<pre>\n";
            ss << "time to get readlock: " << millis << "ms\n";

            ss << "# databases: " << dbHolder.size() << '\n';

            if( ClientCursor::numCursors()>500 )
                ss << "# Cursors: " << ClientCursor::numCursors() << '\n';

            ss << "\nreplication: ";
            if( *replInfo )
                ss << "\nreplInfo:  " << replInfo << "\n\n";
            if( replSet ) {
                ss << a("", "see replSetGetStatus link top of page") << "--replSet </a>" << cmdLine._replSet << '\n';
            }
            if ( replAllDead )
                ss << "<b>replication replAllDead=" << replAllDead << "</b>\n";

            else {
                ss << "\nmaster: " << replSettings.master << '\n';
                ss << "slave:  " << replSettings.slave << '\n';
                ss << '\n';
            }

            BackgroundOperation::dump(ss);
            ss << "</pre>\n";
        }

        virtual void run( stringstream& ss ) {
            Timer t;
            readlocktry lk( "" , 300 );
            if ( lk.got() ) {
                _gotLock( t.millis() , ss );
            }
            else {
                ss << "\n<b>timed out getting lock</b>\n";
            }
        }
    } lowLevelMongodStatus;
}
