package com.andr3h3nriqu3s.applications

import java.sql.ResultSet
import java.text.SimpleDateFormat
import java.util.Date
import java.util.UUID
import kotlin.collections.emptyList
import kotlin.collections.setOf
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException

data class Application(
        var id: String,
        var url: String,
        var original_url: String?,
        var unique_url: String?,
        var title: String,
        var user_id: String,
        var extra_data: String,
        var payrange: String,
        var status: Int,
        var company: String,
        var recruiter: String,
        var message: String,
        var linked_application: String,
        var status_history: String,
        var application_time: String,
        var create_time: String,
        var flairs: List<Flair>,
        var views: List<View>,
) {
    companion object : RowMapper<Application> {
        override public fun mapRow(rs: ResultSet, rowNum: Int): Application {
            return Application(
                    rs.getString("id"),
                    rs.getString("url"),
                    rs.getString("original_url"),
                    rs.getString("unique_url"),
                    rs.getString("title"),
                    rs.getString("user_id"),
                    rs.getString("extra_data"),
                    rs.getString("payrange"),
                    rs.getInt("status"),
                    rs.getString("company"),
                    rs.getString("recruiter"),
                    rs.getString("message"),
                    rs.getString("linked_application"),
                    rs.getString("status_history"),
                    rs.getString("application_time"),
                    rs.getString("create_time"),
                    emptyList(),
                    emptyList(),
            )
        }
    }
}

data class SubmitRequest(val text: String)

data class ListRequest(val status: Int?)

data class StatusRequest(val id: String, val status: Int)

data class FlairRequest(val id: String, val text: String)

data class UpdateUrl(val id: String, val url: String)

data class CVData(
        val company: String,
        val recruiter: String,
        val message: String,
        val flairs: List<SimpleFlair>
)

@RestController
@ControllerAdvice
@RequestMapping("/api/application")
class ApplicationsController(
        val sessionService: SessionService,
        val applicationService: ApplicationService,
        val flairService: FlairService,
        val viewService: ViewService,
) {

    @GetMapping(path = ["/cv/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun getCV(@PathVariable id: String, @RequestHeader("token") token: String?): CVData? {
        val user = sessionService.verifyToken(token)

        val application = applicationService.findApplicationByIdNoUser(id)

        if (application == null) return null

        if (user == null) {
            viewService.create(application.id)
        }

        val flairs = application.flairs.map { it.toFlairSimple() }

        return CVData(application.company, application.recruiter, application.message, flairs)
    }

    /** Create a new application from the link */
    @PostMapping(path = ["/link"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun submitLink(
            @RequestBody submit: SubmitRequest,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)

        var application =
                Application(
                        UUID.randomUUID().toString(),
                        submit.text,
                        submit.text,
                        submit.text,
                        "New Application",
                        user.id,
                        "",
                        "",
                        0,
                        "",
                        "",
                        "",
                        "",
                        "",
                        "",
                        "",
                        emptyList(),
                        emptyList(),
                )

        if (!applicationService.createApplication(user, application)) {
            throw ResponseStatusException(HttpStatus.CONFLICT, "Application already exists", null)
        }

        print("Created: ")
        println(application)

        return application
    }

    @PostMapping(path = ["/text/flair"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun textFlair(
            @RequestBody info: FlairRequest,
            @RequestHeader("token") token: String
    ): Int {
        val user = sessionService.verifyTokenThrow(token)
        val application = applicationService.findApplicationById(user, info.id)
        if (application == null) {
            throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
        }

        val flairs = flairService.listUser(user)

        var count = 0

        for (flair: Flair in flairs) {
            val regex =
                    Regex(
                            ".*" + flair.expr + ".*",
                            setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
                    )

            if (regex.matches(info.text)) {
                count += 1
                flairService.linkFlair(application, flair)
            }
        }

        return count
    }

    @PostMapping(path = ["/text"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun submitText(
            @RequestBody submit: SubmitRequest,
            @RequestHeader("token") token: String
    ): Int {
        val user = sessionService.verifyTokenThrow(token)

        var text = submit.text.replace("=\n", "")

        var urls: List<String> = emptyList()

        while (true) {
            var index = text.indexOf("href")

            if (index == -1) {
                break
            }

            var new_url = StringBuilder()

            var found_start = false

            while (true) {
                if (found_start) {
                    if (text[index] == '"') {
                        break
                    }
                    new_url.append(text[index])
                } else if (text[index] == '"') {
                    found_start = true
                }
                index++
            }

            text = text.substring(index)

            urls = urls.plus(new_url.toString().replace("&amp;", "&").replace("=3D", "="))
        }

        print("found: ")
        print(urls.size)
        print(" links\n")

        // jobListing is for glassdoor urls
        // jobs/view is for linkeding urls
        urls =
                urls.filter { predicate ->
                    print("url: ")
                    print(predicate)
                    print("\n")
                    predicate.contains("jobListing") || predicate.contains("jobs/view")
                }

        print("found fileted: ")
        print(urls.size)
        print(" links\n")

        urls = urls.toSet().toList()

        print("removed duplicates: ")
        print(urls.size)
        print(" links\n")

        var applications =
                urls.map { elm ->
                    Application(
                            UUID.randomUUID().toString(),
                            if (elm.contains("linkedin")) elm.split("?")[0] else elm,
                            if (elm.contains("linkedin")) elm.split("?")[0] else null,
                            if (elm.contains("linkedin")) elm.split("?")[0] else null,
                            "New Application",
                            user.id,
                            "",
                            "",
                            0,
                            "",
                            "",
                            "",
                            "",
                            "",
                            "",
                            "",
                            emptyList(),
                            emptyList(),
                    )
                }

        applications =
                applications.filter { elm -> applicationService.createApplication(user, elm) }

        print("created new: ")
        print(applications.size)
        print(" links\n")

        return applications.size
    }

    @PostMapping(path = ["/list"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun list(
            @RequestBody info: ListRequest,
            @RequestHeader("token") token: String
    ): List<Application> {
        val user = sessionService.verifyTokenThrow(token)
        return applicationService.findAll(user, info)
    }

    @GetMapping(path = ["/active"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun active(@RequestHeader("token") token: String): Application? {
        val user = sessionService.verifyTokenThrow(token)
        val possibleApplications = applicationService.findAll(user, ListRequest(1))
        if (possibleApplications.size == 0) {
            return null
        }
        return applicationService.findApplicationById(user, possibleApplications[0].id)
    }

    @PutMapping(path = ["/status"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun status(
            @RequestBody info: StatusRequest,
            @RequestHeader("token") token: String,
    ): Application {
        val user = sessionService.verifyTokenThrow(token)
        var application = applicationService.findApplicationById(user, info.id)
        if (application == null) {
            throw NotFound()
        }

        application.status = info.status
        val status_string = "${info.status}"
        var status_history = application.status_history.split(",").filter { it.length >= 1 }
        if (status_history.indexOf(status_string) == -1) {
            status_history = status_history.plus("${info.status}")
        }

        application.status_history = status_history.joinToString(",") { it }

        if (info.status == 4) {
            val sdf = SimpleDateFormat("dd/MM/yyyy hh:mm:ss")
            application.application_time = sdf.format(Date())
        }

        applicationService.update(application)

        return application
    }

    @PutMapping(path = ["/update"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun update(
            @RequestBody info: Application,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)
        var application = applicationService.findApplicationById(user, info.id)
        if (application == null) {
            throw NotFound()
        }
        applicationService.update(info)
        return info
    }

    @PostMapping(path = ["/update/url"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun updateUrl(
            @RequestBody info: UpdateUrl,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)
        var application = applicationService.findApplicationById(user, info.id)

        if (application == null) {
            throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
        }

        if (application.unique_url != null) {
            throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Application already has unique_url", null)
        }

        application.original_url = application.url
        application.url = info.url
        application.unique_url = info.url.split("?")[0]

        var maybe_exists =
                applicationService.findApplicationByUrl(
                        user,
                        application.url,
                        application.unique_url
                )
        if (maybe_exists != null) {
            applicationService.delete(application)

            if (maybe_exists.status == 0 && application.status == 1) {
                maybe_exists.status = 1
                applicationService.update(maybe_exists)
            }

            maybe_exists.flairs = flairService.listFromLinkApplicationId(maybe_exists.id)

            return maybe_exists
        }

        applicationService.update(application)

        application.flairs = flairService.listFromLinkApplicationId(application.id)
        application.views = viewService.listFromApplicationId(application.id)

        return application
    }

    @PostMapping(path = ["/reset/url/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun updateUrl(
            @PathVariable id: String,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)
        var application = applicationService.findApplicationById(user, id)

        if (application == null) {
            throw NotFound()
        }

        if (application.unique_url == null) {
            throw BadRequest()
        }

        application.url = application.original_url!!
        application.original_url = null
        application.unique_url = null

        applicationService.update(application)

        application.flairs = flairService.listFromLinkApplicationId(application.id)

        return application
    }

    @DeleteMapping(path = ["/flair/{id}/{flairid}"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun delete(
            @PathVariable id: String,
            @PathVariable flairid: String,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)

        val application = applicationService.findApplicationById(user, id)
        if (application == null) {
            throw NotFound()
        }

        flairService.unlinkFlair(id, flairid)
        return applicationService.findApplicationById(user, id)!!
    }

    @DeleteMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun delete(
            @PathVariable id: String,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)

        val application = applicationService.findApplicationById(user, id)
        if (application == null) {
            throw NotFound()
        }

        applicationService.delete(application)
        return application
    }
}

@Service
class ApplicationService(
        val db: JdbcTemplate,
        val flairService: FlairService,
        val viewService: ViewService
) {

    public fun findApplicationByUrl(user: UserDb, url: String, unique_url: String?): Application? {
        if (unique_url != null) {
            val unique: List<Application> =
                    db.query(
                                    "select * from applications where unique_url=? and user_id=?",
                                    arrayOf(unique_url, user.id),
                                    Application
                            )
                            .toList()

            if (unique.size != 0) {
                return unique[0]
            }
        }

        val applications: List<Application> =
                db.query(
                                "select * from applications where url=? and user_id=?",
                                arrayOf(url, user.id),
                                Application
                        )
                        .toList()

        if (applications.size == 0) {
            return null
        }

        return applications[0]
    }

    public fun findApplicationById(user: UserDb, id: String): Application? {
        var applications =
                db.query(
                                "select * from applications where id=? and user_id=?",
                                arrayOf(id, user.id),
                                Application
                        )
                        .toList()

        if (applications.size == 0) {
            return null
        }

        var application = applications[0]

        application.flairs = flairService.listFromLinkApplicationId(application.id)
        application.views = viewService.listFromApplicationId(application.id)

        return application
    }

    public fun findApplicationByIdNoUser(id: String): Application? {
        var applications =
                db.query("select * from applications where id=?", arrayOf(id), Application).toList()

        if (applications.size == 0) {
            return null
        }

        var application = applications[0]

        // Views are not needed for this request
        application.flairs = flairService.listFromLinkApplicationId(application.id)

        return application
    }

    public fun createApplication(user: UserDb, application: Application): Boolean {
        if (this.findApplicationByUrl(user, application.url, application.unique_url) != null) {
            return false
        }

        // Create time is auto created by the database
        db.update(
                "insert into applications (id, url, original_url, unique_url, title, user_id, extra_data, payrange, status, company, recruiter, message, linked_application, status_history, application_time) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
                application.id,
                application.url,
                application.original_url,
                application.unique_url,
                application.title,
                application.user_id,
                application.extra_data,
                application.payrange,
                application.status,
                application.company,
                application.recruiter,
                application.message,
                application.linked_application,
                application.status_history,
                application.application_time,
        )

        return true
    }

    public fun findAll(user: UserDb, info: ListRequest): List<Application> {
        if (info.status == null) {
            return db.query(
                            "select * from applications where user_id=? order by title asc;",
                            arrayOf(user.id),
                            Application
                    )
                    .toList()
        }

        // If it's to apply also remove the linked_application to only show the main
        if (info.status == 0) {
            return db.query(
                            "select * from applications where user_id=? and linked_application='' and status=0 order by title asc;",
                            arrayOf(user.id),
                            Application
                    )
                    .toList()
        }

        return db.query(
                        "select * from applications where user_id=? and status=? order by title asc;",
                        arrayOf(user.id, info.status),
                        Application,
                )
                .toList()
    }

    public fun update(application: Application): Application {
        // I don't want ot update create_time
        db.update(
                "update applications set url=?, original_url=?, unique_url=?, title=?, user_id=?, extra_data=?, payrange=?, status=?, company=?, recruiter=?, message=?, linked_application=?, status_history=?, application_time=? where id=?",
                application.url,
                application.original_url,
                application.unique_url,
                application.title,
                application.user_id,
                application.extra_data,
                application.payrange,
                application.status,
                application.company,
                application.recruiter,
                application.message,
                application.linked_application,
                application.status_history,
                application.application_time,
                application.id,
        )
        return application
    }

    public fun delete(application: Application) {
        db.update(
                "delete from applications where id=?",
                application.id,
        )
    }
}
