/*
 * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */


import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;

/**
 * Representation of HTTP response.
 *
 */
public class HttpResponse extends HttpData {

    private String statusCode;

    private String reasonPhrase;

    protected HttpResponse() {

    }

    /**
     * Build HTTP response instance.
     *
     * @param statusLine Status line
     * @param headers    HTTP response headers
     * @param is         InputStream instance for reading HTTP request data
     * @return HttpResponse instance
     * @throws Exception If something is wrong
     */
    public static HttpResponse build(String statusLine, Map<String, List<String>> headers, InputStream is)
            throws Exception {
        HttpResponse httpResponse = new HttpResponse();
        httpResponse.parseStatusLine(statusLine);
        httpResponse.parseHeaders(headers);
        httpResponse.readBody(is);
        httpResponse.processChunkedEncodedResponse();
        return httpResponse;
    }

    private void parseStatusLine(String statusLine) throws Exception {
        String[] parts = statusLine.split("\\s");

        if (parts.length < 3)
            throw new Exception("Cannot parse status line: \"" + statusLine + "\"");

        httpVersion = parts[0];
        if (httpVersion == null)
            throw new Exception("Cannot parse status line: \"" + statusLine + "\". Invalid method.");

        statusCode = parts[1];
        reasonPhrase = parts[2];
    }

    private void parseHeaders(Map<String, List<String>> headers) {
        this.headers.clear();
        for (Map.Entry<String, List<String>> headerEntry : headers.entrySet()) {
            String headerName = headerEntry.getKey();
            if (headerName == null)
                continue;
            this.headers.put(headerName.toLowerCase(), headerEntry.getValue().iterator().next());
        }
    }

    private void readBody(InputStream is) throws Exception {
        String headerValue = getHeader("Content-Length");
        if (headerValue != null) {
            int contentLength = Integer.valueOf(headerValue);
            body = new byte[contentLength];
            int read = readBytes(is, body, contentLength);
            if (contentLength != read) {
                throw new Exception("Content-length (" + contentLength + ") is not equal actual length of read data ("
                        + read + ")");
            }
        } else {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            BufferedInputStream bis = new BufferedInputStream(is);
            int read;
            byte[] buffer = new byte[4096];
            while ((read = bis.read(buffer, 0, buffer.length)) > 0) {
                bos.write(buffer, 0, read);
            }
            body = bos.toByteArray();
        }
    }

    private void processChunkedEncodedResponse() {
        boolean chunked = false;
        for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
            String headerName = headerEntry.getKey();
            String headerValue = headerEntry.getValue();
            if ("transfer-encoding".equals(headerName) && "chunked".equals(headerValue.toLowerCase())) {
                chunked = true;
                break;
            }
        }

        if (chunked) {
            // It is expected that chunked encoded data has been processed in
            // sun.net.www.http.ChunkedInputStream, so remove this header and set content
            // length
            headers.remove("transfer-encoding");
            headers.put("content-length", String.valueOf(body.length));
        }
    }

    public byte[] getBytes() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        bos.write(httpVersion.getBytes());
        bos.write(" ".getBytes());
        bos.write(statusCode.getBytes());
        bos.write(" ".getBytes());
        bos.write(reasonPhrase.getBytes());
        bos.write(CRLF_BYTES);
        for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
            bos.write(headerEntry.getKey().getBytes());
            bos.write(": ".getBytes());
            bos.write(headerEntry.getValue().getBytes());
            bos.write(CRLF_BYTES);
        }
        bos.write(CRLF_BYTES);
        bos.write(body);
        bos.flush();
        return bos.toByteArray();
    }

    public void write(OutputStream out) throws IOException {
        out.write(getBytes());
    }

    @Override
    public String toString() {
        return "HttpResponse " + "[" + "httpVersion = " + httpVersion + ", statusCode = " + statusCode
                + ", httpVersion = " + httpVersion + ", reasonPhrase = " + reasonPhrase + ", headers = "
                + headers.toString() + ", body (length = " + body.length + ") = {" + body + "}" + "]";
    }
}
